summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/travelynx.conf (renamed from .travis.travelynx.conf)6
-rw-r--r--.github/workflows/perl.yml50
-rw-r--r--.mailmap1
-rw-r--r--.reuse/dep572
-rw-r--r--.travis.yml20
-rw-r--r--COPYING624
-rw-r--r--Dockerfile54
-rw-r--r--LICENSES/AGPL-3.0-or-later.txt603
-rw-r--r--LICENSES/Apache-2.0.txt183
-rw-r--r--LICENSES/BSD-2-Clause.txt22
-rw-r--r--LICENSES/CC-BY-4.0.txt324
-rw-r--r--LICENSES/CC0-1.0.txt1520
-rw-r--r--LICENSES/MIT.txt20
-rw-r--r--README.md125
-rwxr-xr-xcontrib/i3bar-snippet.py111
-rw-r--r--contrib/polybar.sh201
-rw-r--r--cpanfile14
-rw-r--r--cpanfile.snapshot2667
-rw-r--r--docker-compose.yml6
-rwxr-xr-xdocker-run.sh70
-rw-r--r--examples/docker/email-transport.sh5
-rw-r--r--examples/docker/travelynx.conf26
-rw-r--r--examples/nginx/travelynx.conf40
-rw-r--r--examples/travelynx.conf111
-rw-r--r--index.pl3
-rwxr-xr-xlib/Travelynx.pm4128
-rw-r--r--lib/Travelynx/Command/account.pm119
-rw-r--r--lib/Travelynx/Command/database.pm1291
-rw-r--r--lib/Travelynx/Command/dumpconfig.pm3
-rw-r--r--lib/Travelynx/Command/dumpstops.pm51
-rw-r--r--lib/Travelynx/Command/influxdb.pm209
-rw-r--r--lib/Travelynx/Command/integritycheck.pm112
-rw-r--r--lib/Travelynx/Command/maintenance.pm105
-rw-r--r--lib/Travelynx/Command/munin.pm46
-rw-r--r--lib/Travelynx/Command/traewelling.pm207
-rw-r--r--lib/Travelynx/Command/work.pm261
-rw-r--r--lib/Travelynx/Command/worker.pm27
-rw-r--r--lib/Travelynx/Controller/Account.pm1111
-rwxr-xr-xlib/Travelynx/Controller/Api.pm409
-rw-r--r--lib/Travelynx/Controller/Passengerrights.pm24
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm603
-rw-r--r--lib/Travelynx/Controller/Static.pm20
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm154
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1982
-rw-r--r--lib/Travelynx/Helper/DBDB.pm150
-rw-r--r--lib/Travelynx/Helper/HAFAS.pm315
-rw-r--r--lib/Travelynx/Helper/IRIS.pm237
-rw-r--r--lib/Travelynx/Helper/Sendmail.pm52
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm387
-rw-r--r--lib/Travelynx/Model/InTransit.pm900
-rwxr-xr-xlib/Travelynx/Model/JourneyStatsCache.pm122
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm1785
-rw-r--r--lib/Travelynx/Model/Stations.pm199
-rw-r--r--lib/Travelynx/Model/Traewelling.pm243
-rw-r--r--lib/Travelynx/Model/Users.pm1160
-rw-r--r--public/service-worker.js25
-rw-r--r--public/static/api.yml267
-rw-r--r--public/static/css/dark.min.css20
-rw-r--r--public/static/css/light.min.css20
-rw-r--r--public/static/css/local.css46
-rw-r--r--public/static/css/material-icons.css8
-rw-r--r--public/static/fonts/MaterialIcons-Regular.svg6
-rw-r--r--public/static/icons/touch-icon-120x120.pngbin0 -> 2763 bytes
-rw-r--r--public/static/icons/touch-icon-128x128.pngbin0 -> 2854 bytes
-rw-r--r--public/static/icons/touch-icon-144x144.pngbin0 -> 3080 bytes
-rw-r--r--public/static/icons/touch-icon-152x152.pngbin0 -> 3241 bytes
-rw-r--r--public/static/icons/touch-icon-167x167.pngbin0 -> 3435 bytes
-rw-r--r--public/static/icons/touch-icon-16x16.pngbin0 -> 870 bytes
-rw-r--r--public/static/icons/touch-icon-180x180.pngbin0 -> 3721 bytes
-rw-r--r--public/static/icons/touch-icon-192x192.pngbin0 -> 3891 bytes
-rw-r--r--public/static/icons/touch-icon-256x256.pngbin0 -> 4915 bytes
-rw-r--r--public/static/icons/touch-icon-32x32.pngbin0 -> 1232 bytes
-rw-r--r--public/static/icons/touch-icon-512x512.pngbin0 -> 9462 bytes
-rw-r--r--public/static/icons/touch-icon-96x96.pngbin0 -> 2317 bytes
-rw-r--r--public/static/icons/touch-icon.svg1
-rw-r--r--public/static/js/autocomplete.js8642
-rw-r--r--public/static/js/autocomplete.min.js6
-rw-r--r--public/static/js/geolocation.js66
-rw-r--r--public/static/js/geolocation.min.js2
-rw-r--r--public/static/js/travelynx-actions.js89
-rw-r--r--public/static/js/travelynx-actions.min.js2
-rw-r--r--public/static/manifest.json12
l---------public/static/v71 (renamed from public/static/v31)0
l---------public/static/v72 (renamed from public/static/v32)0
-rw-r--r--sass/.prettierrc7
-rw-r--r--sass/components/_badges.scss55
-rw-r--r--sass/components/_buttons.scss322
-rw-r--r--sass/components/_cards.scss195
-rw-r--r--sass/components/_carousel.scss90
-rw-r--r--sass/components/_chips.scss90
-rw-r--r--sass/components/_collapsible.scss91
-rw-r--r--sass/components/_color-classes.scss32
-rw-r--r--sass/components/_color-variables.scss370
-rw-r--r--sass/components/_datepicker.scss191
-rw-r--r--sass/components/_dropdown.scss85
-rw-r--r--sass/components/_global.scss769
-rw-r--r--sass/components/_grid.scss156
-rw-r--r--sass/components/_icons-material-design.scss5
-rw-r--r--sass/components/_materialbox.scss43
-rw-r--r--sass/components/_modal.scss94
-rw-r--r--sass/components/_navbar.scss208
-rw-r--r--sass/components/_normalize.scss447
-rw-r--r--sass/components/_preloader.scss334
-rw-r--r--sass/components/_pulse.scss34
-rw-r--r--sass/components/_sidenav.scss216
-rw-r--r--sass/components/_slider.scss92
-rw-r--r--sass/components/_table_of_contents.scss33
-rw-r--r--sass/components/_tabs.scss99
-rw-r--r--sass/components/_tapTarget.scss103
-rw-r--r--sass/components/_timepicker.scss183
-rw-r--r--sass/components/_toast.scss58
-rw-r--r--sass/components/_tooltip.scss32
-rw-r--r--sass/components/_transitions.scss13
-rw-r--r--sass/components/_typography.scss60
-rw-r--r--sass/components/_variables.scss350
-rw-r--r--sass/components/_waves.scss114
-rw-r--r--sass/components/forms/_checkboxes.scss200
-rw-r--r--sass/components/forms/_file-input.scss44
-rw-r--r--sass/components/forms/_forms.scss22
-rw-r--r--sass/components/forms/_input-fields.scss354
-rw-r--r--sass/components/forms/_radio-buttons.scss115
-rw-r--r--sass/components/forms/_range.scss161
-rw-r--r--sass/components/forms/_select.scss180
-rw-r--r--sass/components/forms/_switches.scss89
-rw-r--r--sass/materialize.scss41
-rw-r--r--sass/package-lock.json7153
-rw-r--r--sass/package.json32
-rw-r--r--sass/src/common/index.scss41
-rw-r--r--sass/src/common/local.scss297
-rw-r--r--sass/src/dark/_variables.scss14
-rw-r--r--sass/src/dark/index.scss5
-rw-r--r--sass/src/light/_variables.scss5
-rw-r--r--sass/src/light/index.scss5
-rw-r--r--sass/webpack.config.js47
-rwxr-xr-xscripts/asset-rebuild10
-rwxr-xr-xscripts/asset-release4
-rwxr-xr-xshare/ice_names.json254
-rwxr-xr-xshare/old_station_names.json136
-rw-r--r--share/old_stations.json2603
-rw-r--r--t/01-static.t5
-rw-r--r--t/02-registration.t124
-rw-r--r--t/11-journey-stats.t178
-rw-r--r--t/12-journey-edit.t206
-rw-r--r--t/21-relations.t855
-rw-r--r--t/22-transit-visibility.t487
-rw-r--r--t/23-journey-visibility.t461
-rw-r--r--t/24-past-visibility.t558
-rw-r--r--t/r-negative-delay.t101
-rw-r--r--templates/_cancelled.html.ep27
-rw-r--r--templates/_cancelled_departure.html.ep13
-rw-r--r--templates/_checked_in.html.ep322
-rw-r--r--templates/_checked_out.html.ep18
-rw-r--r--templates/_connections.html.ep123
-rw-r--r--templates/_connections_hafas.html.ep48
-rw-r--r--templates/_departures_hafas.html.ep53
-rw-r--r--templates/_departures_iris.html.ep58
-rw-r--r--templates/_footer.html.ep9
-rw-r--r--templates/_format_train.html.ep10
-rw-r--r--templates/_history_months.html.ep21
-rw-r--r--templates/_history_months_for_year.html.ep18
-rw-r--r--templates/_history_stats.html.ep45
-rw-r--r--templates/_history_trains.html.ep87
-rw-r--r--templates/_history_years.html.ep22
-rw-r--r--templates/_history_years_list.html.ep13
-rw-r--r--templates/_invalid_input.html.ep13
-rw-r--r--templates/_map.html.ep28
-rw-r--r--templates/_public_status_card.html.ep125
-rw-r--r--templates/_timeline-checked-in.html.ep14
-rw-r--r--templates/_timeline_link.html.ep16
-rw-r--r--templates/_wagons.html.ep8
-rw-r--r--templates/about.html.ep15
-rw-r--r--templates/account.html.ep208
-rw-r--r--templates/add_journey.html.ep19
-rw-r--r--templates/api_documentation.html.ep109
-rw-r--r--templates/bad_request.html.ep19
-rw-r--r--templates/change_name.html.ep47
-rw-r--r--templates/change_password.html.ep4
-rw-r--r--templates/changelog.html.ep557
-rw-r--r--templates/commute.html.ep91
-rw-r--r--templates/departures.html.ep166
-rw-r--r--templates/disambiguation.html.ep20
-rw-r--r--templates/edit_comment.html.ep6
-rw-r--r--templates/edit_journey.html.ep34
-rw-r--r--templates/edit_profile.html.ep60
-rw-r--r--templates/edit_visibility.html.ep123
-rw-r--r--templates/exception.html.ep2
-rw-r--r--templates/history.html.ep52
-rw-r--r--templates/history_by_month.html.ep4
-rw-r--r--templates/history_by_year.html.ep29
-rw-r--r--templates/history_map.html.ep130
-rw-r--r--templates/journey.html.ep207
-rw-r--r--templates/landingpage.html.ep56
-rw-r--r--templates/layouts/default.html.ep83
-rw-r--r--templates/legend.html.ep111
-rw-r--r--templates/login.html.ep20
-rw-r--r--templates/not_found.html.ep7
-rw-r--r--templates/privacy.html.ep167
-rw-r--r--templates/profile.html.ep92
-rw-r--r--templates/register.html.ep21
-rw-r--r--templates/social.html.ep68
-rw-r--r--templates/social_list.html.ep259
-rw-r--r--templates/timeline-checked-in.html.ep3
-rw-r--r--templates/traewelling.html.ep256
-rw-r--r--templates/use_external_links.html.ep82
-rw-r--r--templates/use_history.html.ep4
-rw-r--r--templates/user_status.html.ep2
-rw-r--r--templates/webhooks.html.ep9
-rw-r--r--templates/year_in_review.html.ep169
-rwxr-xr-xupdate.sh25
209 files changed, 35115 insertions, 21572 deletions
diff --git a/.travis.travelynx.conf b/.github/travelynx.conf
index 2678d10..816926e 100644
--- a/.travis.travelynx.conf
+++ b/.github/travelynx.conf
@@ -4,9 +4,9 @@
realtime => '/tmp/dbf-iris-realtime',
},
db => {
- host => 'localhost',
+ host => 'postgres',
database => 'travelynx_ci_test',
- user => 'postgres',
- password => '',
+ user => 'travelynx',
+ password => 'whatever',
},
};
diff --git a/.github/workflows/perl.yml b/.github/workflows/perl.yml
new file mode 100644
index 0000000..2e64b35
--- /dev/null
+++ b/.github/workflows/perl.yml
@@ -0,0 +1,50 @@
+name: Perl Tests
+
+on:
+ push:
+ branches:
+ - '*'
+ pull_request:
+ branches:
+ - '*'
+
+jobs:
+ perl:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ perl-version:
+ - '5.20'
+ - 'latest'
+ - 'threaded'
+
+ container:
+ image: perl:${{ matrix.perl-version }}
+
+ services:
+ postgres:
+ image: postgres:11
+ env:
+ POSTGRES_USER: travelynx
+ POSTGRES_PASSWORD: whatever
+ POSTGRES_DB: travelynx_ci_test
+ ports:
+ - 5432:5432
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: perl -V
+ run: perl -V
+ - name: Setup Repo
+ run: |
+ rm -f cpanfile.snapshot
+ cp .github/travelynx.conf travelynx.conf
+ - name: Install PostgreSQL Client Library
+ run: apt install libpq-dev
+ - name: Install Perl Dependencies
+ run: curl -sL https://raw.githubusercontent.com/skaji/cpm/master/cpm | perl - install -g --show-build-log-on-failure
+ - name: Run Tests
+ run: prove -l t
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..c69b0d4
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1 @@
+Birte Kristina Friesel <derf@finalrewind.org>
diff --git a/.reuse/dep5 b/.reuse/dep5
new file mode 100644
index 0000000..b152498
--- /dev/null
+++ b/.reuse/dep5
@@ -0,0 +1,72 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files: .dockerignore .github/* .github/*/* .gitignore
+Copyright: 2020 Birte Kristina Friesel
+License: CC0-1.0
+
+Files: Dockerfile README.md cpanfile cpanfile.snapshot
+Copyright: 2020 Birte Kristina Friesel
+License: CC0-1.0
+
+Files: examples/*
+Copyright: 2020 Birte Kristina Friesel
+License: CC0-1.0
+
+Files: public/static/css/material-icons.css public/static/fonts/MaterialIcons-*
+Copyright: 2014-2019 Materialize
+License: Apache-2.0
+
+Files: public/static/fonts/roboto/*
+Copyright: 2012 Google
+License: Apache-2.0
+
+Files: public/static/icons/*
+Copyright: 2014-2019 Materialize
+License: Apache-2.0
+
+Files: public/static/js/autocomplete.min.js
+Copyright: 2020 DB Station&Service AG, Europaplatz 1, 10557 Berlin
+ 2020 Birte Kristina Friesel
+License: CC-BY-4.0
+
+Files: public/static/js/geolocation.min.js public/static/js/travelynx-actions.js
+Copyright: 2020 Birte Kristina Friesel
+License: MIT
+
+Files: public/static/js/jquery-3.4.1.min.js
+Copyright: 2020 OpenJS Foundation and other contributors, https://openjsf.org/
+License: MIT
+
+Files: public/static/js/materialize.min.js
+Copyright: 2014-2019 Materialize
+License: Apache-2.0
+
+Files: public/static/leaflet/*
+Copyright: 2010-2019 Vladimir Agafonkin
+ 2010-2011 CloudMade
+License: BSD-2-Clause
+
+Files: public/static/manifest.json
+Copyright: 2020 Birte Kristina Friesel
+License: CC0-1.0
+
+Files: templates/*
+Copyright: 2020 Birte Kristina Friesel
+License: MIT
+
+Files: sass/materialize.scss sass/components/*
+Copyright: 2014-2019 Materialize
+License: MIT
+
+Files: sass/src/*
+Copyright: 2019 marudor
+ 2020 Birte Kristina Friesel
+License: MIT
+
+Files: share/ice_names.json
+Copyright: 2017 marudor
+License: MIT
+
+Files: share/old_station_names.json
+Copyright: 2020 Birte Kristina Friesel
+License: CC0-1.0
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2ae0fe4..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-language: perl
-perl:
- - "5.30"
- - "5.28"
- - "5.26"
-services:
- - postgresql
-addons:
- apt:
- packages:
- - libcache-perl
- - libdb-dev
- - libpq-dev
- - libxml2-dev
- postgresql: "9.4"
-before_script:
- - cp .travis.travelynx.conf travelynx.conf
- - psql -c 'create database travelynx_ci_test;' -U postgres
-script:
- - prove -lv
diff --git a/COPYING b/COPYING
index 7945a1a..3e275d8 100644
--- a/COPYING
+++ b/COPYING
@@ -1,21 +1,603 @@
-MIT License
-
-Copyright (c) 2018-2019 Daniel Friesel
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+GNU AFFERO GENERAL PUBLIC LICENSE
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+ Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software
+and other kinds of works, specifically designed to ensure cooperation with
+the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take
+away your freedom to share and change the works. By contrast, our General
+Public Licenses are intended to guarantee your freedom to share and change
+all versions of a program--to make sure it remains free software for all its
+users.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for them if you wish), that
+you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs, and that you know you
+can do these things.
+
+Developers that use our General Public Licenses protect your rights with two
+steps: (1) assert copyright on the software, and (2) offer you this License
+which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made
+in alternate versions of the program, if they receive widespread use, become
+available for other developers to incorporate. Many developers of free software
+are heartened and encouraged by the resulting cooperation. However, in the
+case of software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and letting
+the public access it on a server without ever releasing its source code to
+the public.
+
+The GNU Affero General Public License is designed specifically to ensure that,
+in such cases, the modified source code becomes available to the community.
+It requires the operator of a network server to provide the source code of
+the modified version running there to the users of that server. Therefore,
+public use of a modified version, on a publicly accessible server, gives the
+public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by
+Affero, was designed to accomplish similar goals. This is a different license,
+not a version of the Affero GPL, but Affero has released a new version of
+the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+ TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works,
+such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License.
+Each licensee is addressed as "you". "Licensees" and "recipients" may be
+individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in
+a fashion requiring copyright permission, other than the making of an exact
+copy. The resulting work is called a "modified version" of the earlier work
+or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the
+Program.
+
+To "propagate" a work means to do anything with it that, without permission,
+would make you directly or secondarily liable for infringement under applicable
+copyright law, except executing it on a computer or modifying a private copy.
+Propagation includes copying, distribution (with or without modification),
+making available to the public, and in some countries other activities as
+well.
+
+To "convey" a work means any kind of propagation that enables other parties
+to make or receive copies. Mere interaction with a user through a computer
+network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the
+extent that it includes a convenient and prominently visible feature that
+(1) displays an appropriate copyright notice, and (2) tells the user that
+there is no warranty for the work (except to the extent that warranties are
+provided), that licensees may convey the work under this License, and how
+to view a copy of this License. If the interface presents a list of user
+commands or options, such as a menu, a prominent item in the list meets this
+criterion.
+
+1. Source Code.
+The "source code" for a work means the preferred form of the work for making
+modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard
+defined by a recognized standards body, or, in the case of interfaces specified
+for a particular programming language, one that is widely used among developers
+working in that language.
+
+The "System Libraries" of an executable work include anything, other than
+the work as a whole, that (a) is included in the normal form of packaging
+a Major Component, but which is not part of that Major Component, and (b)
+serves only to enable use of the work with that Major Component, or to implement
+a Standard Interface for which an implementation is available to the public
+in source code form. A "Major Component", in this context, means a major
+essential component (kernel, window system, and so on) of the specific operating
+system (if any) on which the executable work runs, or a compiler used to produce
+the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source
+code needed to generate, install, and (for an executable work) run the object
+code and to modify the work, including scripts to control those activities.
+However, it does not include the work's System Libraries, or general-purpose
+tools or generally available free programs which are used unmodified in performing
+those activities but which are not part of the work. For example, Corresponding
+Source includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically linked
+subprograms that the work is specifically designed to require, such as by
+intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate
+automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+2. Basic Permissions.
+All rights granted under this License are granted for the term of copyright
+on the Program, and are irrevocable provided the stated conditions are met.
+This License explicitly affirms your unlimited permission to run the unmodified
+Program. The output from running a covered work is covered by this License
+only if the output, given its content, constitutes a covered work. This License
+acknowledges your rights of fair use or other equivalent, as provided by copyright
+law.
+
+You may make, run and propagate covered works that you do not convey, without
+conditions so long as your license otherwise remains in force. You may convey
+covered works to others for the sole purpose of having them make modifications
+exclusively for you, or provide you with facilities for running those works,
+provided that you comply with the terms of this License in conveying all material
+for which you do not control copyright. Those thus making or running the
+covered works for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of your copyrighted
+material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions
+stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+No covered work shall be deemed part of an effective technological measure
+under any applicable law fulfilling obligations under article 11 of the WIPO
+copyright treaty adopted on 20 December 1996, or similar laws prohibiting
+or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention
+of technological measures to the extent such circumvention is effected by
+exercising rights under this License with respect to the covered work, and
+you disclaim any intention to limit operation or modification of the work
+as a means of enforcing, against the work's users, your or third parties'
+legal rights to forbid circumvention of technological measures.
+
+4. Conveying Verbatim Copies.
+You may convey verbatim copies of the Program's source code as you receive
+it, in any medium, provided that you conspicuously and appropriately publish
+on each copy an appropriate copyright notice; keep intact all notices stating
+that this License and any non-permissive terms added in accord with section
+7 apply to the code; keep intact all notices of the absence of any warranty;
+and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you
+may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+You may convey a work based on the Program, or the modifications to produce
+it from the Program, in the form of source code under the terms of section
+4, provided that you also meet all of these conditions:
+
+a) The work must carry prominent notices stating that you modified it, and
+giving a relevant date.
+
+b) The work must carry prominent notices stating that it is released under
+this License and any conditions added under section 7. This requirement modifies
+the requirement in section 4 to "keep intact all notices".
+
+c) You must license the entire work, as a whole, under this License to anyone
+who comes into possession of a copy. This License will therefore apply, along
+with any applicable section 7 additional terms, to the whole of the work,
+and all its parts, regardless of how they are packaged. This License gives
+no permission to license the work in any other way, but it does not invalidate
+such permission if you have separately received it.
+
+d) If the work has interactive user interfaces, each must display Appropriate
+Legal Notices; however, if the Program has interactive interfaces that do
+not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works,
+which are not by their nature extensions of the covered work, and which are
+not combined with it such as to form a larger program, in or on a volume of
+a storage or distribution medium, is called an "aggregate" if the compilation
+and its resulting copyright are not used to limit the access or legal rights
+of the compilation's users beyond what the individual works permit. Inclusion
+of a covered work in an aggregate does not cause this License to apply to
+the other parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+You may convey a covered work in object code form under the terms of sections
+4 and 5, provided that you also convey the machine-readable Corresponding
+Source under the terms of this License, in one of these ways:
+
+a) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by the Corresponding Source fixed
+on a durable physical medium customarily used for software interchange.
+
+b) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by a written offer, valid for
+at least three years and valid for as long as you offer spare parts or customer
+support for that product model, to give anyone who possesses the object code
+either (1) a copy of the Corresponding Source for all the software in the
+product that is covered by this License, on a durable physical medium customarily
+used for software interchange, for a price no more than your reasonable cost
+of physically performing this conveying of source, or (2) access to copy the
+Corresponding Source from a network server at no charge.
+
+c) Convey individual copies of the object code with a copy of the written
+offer to provide the Corresponding Source. This alternative is allowed only
+occasionally and noncommercially, and only if you received the object code
+with such an offer, in accord with subsection 6b.
+
+d) Convey the object code by offering access from a designated place (gratis
+or for a charge), and offer equivalent access to the Corresponding Source
+in the same way through the same place at no further charge. You need not
+require recipients to copy the Corresponding Source along with the object
+code. If the place to copy the object code is a network server, the Corresponding
+Source may be on a different server (operated by you or a third party) that
+supports equivalent copying facilities, provided you maintain clear directions
+next to the object code saying where to find the Corresponding Source. Regardless
+of what server hosts the Corresponding Source, you remain obligated to ensure
+that it is available for as long as needed to satisfy these requirements.
+
+e) Convey the object code using peer-to-peer transmission, provided you inform
+other peers where the object code and Corresponding Source of the work are
+being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from
+the Corresponding Source as a System Library, need not be included in conveying
+the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible
+personal property which is normally used for personal, family, or household
+purposes, or (2) anything designed or sold for incorporation into a dwelling.
+In determining whether a product is a consumer product, doubtful cases shall
+be resolved in favor of coverage. For a particular product received by a
+particular user, "normally used" refers to a typical or common use of that
+class of product, regardless of the status of the particular user or of the
+way in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of whether
+the product has substantial commercial, industrial or non-consumer uses, unless
+such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures,
+authorization keys, or other information required to install and execute modified
+versions of a covered work in that User Product from a modified version of
+its Corresponding Source. The information must suffice to ensure that the
+continued functioning of the modified object code is in no case prevented
+or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically
+for use in, a User Product, and the conveying occurs as part of a transaction
+in which the right of possession and use of the User Product is transferred
+to the recipient in perpetuity or for a fixed term (regardless of how the
+transaction is characterized), the Corresponding Source conveyed under this
+section must be accompanied by the Installation Information. But this requirement
+does not apply if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has been installed
+in ROM).
+
+The requirement to provide Installation Information does not include a requirement
+to continue to provide support service, warranty, or updates for a work that
+has been modified or installed by the recipient, or for the User Product in
+which it has been modified or installed. Access to a network may be denied
+when the modification itself materially and adversely affects the operation
+of the network or violates the rules and protocols for communication across
+the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord
+with this section must be in a format that is publicly documented (and with
+an implementation available to the public in source code form), and must require
+no special password or key for unpacking, reading or copying.
+
+7. Additional Terms.
+"Additional permissions" are terms that supplement the terms of this License
+by making exceptions from one or more of its conditions. Additional permissions
+that are applicable to the entire Program shall be treated as though they
+were included in this License, to the extent that they are valid under applicable
+law. If additional permissions apply only to part of the Program, that part
+may be used separately under those permissions, but the entire Program remains
+governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any
+additional permissions from that copy, or from any part of it. (Additional
+permissions may be written to require their own removal in certain cases when
+you modify the work.) You may place additional permissions on material, added
+by you to a covered work, for which you have or can give appropriate copyright
+permission.
+
+Notwithstanding any other provision of this License, for material you add
+to a covered work, you may (if authorized by the copyright holders of that
+material) supplement the terms of this License with terms:
+
+a) Disclaiming warranty or limiting liability differently from the terms of
+sections 15 and 16 of this License; or
+
+b) Requiring preservation of specified reasonable legal notices or author
+attributions in that material or in the Appropriate Legal Notices displayed
+by works containing it; or
+
+c) Prohibiting misrepresentation of the origin of that material, or requiring
+that modified versions of such material be marked in reasonable ways as different
+from the original version; or
+
+d) Limiting the use for publicity purposes of names of licensors or authors
+of the material; or
+
+e) Declining to grant rights under trademark law for use of some trade names,
+trademarks, or service marks; or
+
+f) Requiring indemnification of licensors and authors of that material by
+anyone who conveys the material (or modified versions of it) with contractual
+assumptions of liability to the recipient, for any liability that these contractual
+assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions"
+within the meaning of section 10. If the Program as you received it, or any
+part of it, contains a notice stating that it is governed by this License
+along with a term that is a further restriction, you may remove that term.
+If a license document contains a further restriction but permits relicensing
+or conveying under this License, you may add to a covered work material governed
+by the terms of that license document, provided that the further restriction
+does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place,
+in the relevant source files, a statement of the additional terms that apply
+to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form
+of a separately written license, or stated as exceptions; the above requirements
+apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided
+under this License. Any attempt otherwise to propagate or modify it is void,
+and will automatically terminate your rights under this License (including
+any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from
+a particular copyright holder is reinstated (a) provisionally, unless and
+until the copyright holder explicitly and finally terminates your license,
+and (b) permanently, if the copyright holder fails to notify you of the violation
+by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently
+if the copyright holder notifies you of the violation by some reasonable means,
+this is the first time you have received notice of violation of this License
+(for any work) from that copyright holder, and you cure the violation prior
+to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses
+of parties who have received copies or rights from you under this License.
+If your rights have been terminated and not permanently reinstated, you do
+not qualify to receive new licenses for the same material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy
+of the Program. Ancillary propagation of a covered work occurring solely
+as a consequence of using peer-to-peer transmission to receive a copy likewise
+does not require acceptance. However, nothing other than this License grants
+you permission to propagate or modify any covered work. These actions infringe
+copyright if you do not accept this License. Therefore, by modifying or propagating
+a covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives
+a license from the original licensors, to run, modify and propagate that work,
+subject to this License. You are not responsible for enforcing compliance
+by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization,
+or substantially all assets of one, or subdividing an organization, or merging
+organizations. If propagation of a covered work results from an entity transaction,
+each party to that transaction who receives a copy of the work also receives
+whatever licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the Corresponding
+Source of the work from the predecessor in interest, if the predecessor has
+it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights
+granted or affirmed under this License. For example, you may not impose a
+license fee, royalty, or other charge for exercise of rights granted under
+this License, and you may not initiate litigation (including a cross-claim
+or counterclaim in a lawsuit) alleging that any patent claim is infringed
+by making, using, selling, offering for sale, or importing the Program or
+any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License
+of the Program or a work on which the Program is based. The work thus licensed
+is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled
+by the contributor, whether already acquired or hereafter acquired, that would
+be infringed by some manner, permitted by this License, of making, using,
+or selling its contributor version, but do not include claims that would be
+infringed only as a consequence of further modification of the contributor
+version. For purposes of this definition, "control" includes the right to
+grant patent sublicenses in a manner consistent with the requirements of this
+License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent
+license under the contributor's essential patent claims, to make, use, sell,
+offer for sale, import and otherwise run, modify and propagate the contents
+of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement
+or commitment, however denominated, not to enforce a patent (such as an express
+permission to practice a patent or covenant not to sue for patent infringement).
+To "grant" such a patent license to a party means to make such an agreement
+or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the
+Corresponding Source of the work is not available for anyone to copy, free
+of charge and under the terms of this License, through a publicly available
+network server or other readily accessible means, then you must either (1)
+cause the Corresponding Source to be so available, or (2) arrange to deprive
+yourself of the benefit of the patent license for this particular work, or
+(3) arrange, in a manner consistent with the requirements of this License,
+to extend the patent
+license to downstream recipients. "Knowingly relying" means you have actual
+knowledge that, but for the patent license, your conveying the covered work
+in a country, or your recipient's use of the covered work in a country, would
+infringe one or more identifiable patents in that country that you have reason
+to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement,
+you convey, or propagate by procuring conveyance of, a covered work, and grant
+a patent license to some of the parties receiving the covered work authorizing
+them to use, propagate, modify or convey a specific copy of the covered work,
+then the patent license you grant is automatically extended to all recipients
+of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope
+of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
+of one or more of the rights that are specifically granted under this License.
+You may not convey a covered work if you are a party to an arrangement with
+a third party that is in the business of distributing software, under which
+you make payment to the third party based on the extent of your activity of
+conveying the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by you
+(or copies made from those copies), or (b) primarily for and in connection
+with specific products or compilations that contain the covered work, unless
+you entered into that arrangement, or that patent license was granted, prior
+to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied
+license or other defenses to infringement that may otherwise be available
+to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise)
+that contradict the conditions of this License, they do not excuse you from
+the conditions of this License. If you cannot convey a covered work so as
+to satisfy simultaneously your obligations under this License and any other
+pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey the
+Program, the only way you could satisfy both those terms and this License
+would be to refrain entirely from conveying the Program.
+
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program,
+your modified version must prominently offer all users interacting with it
+remotely through a computer network (if your version supports such interaction)
+an opportunity to receive the Corresponding Source of your version by providing
+access to the Corresponding Source from a network server at no charge, through
+some standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any work covered
+by version 3 of the GNU General Public License that is incorporated pursuant
+to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to
+link or combine any covered work with a work licensed under version 3 of the
+GNU General Public License into a single combined work, and to convey the
+resulting work. The terms of this License will continue to apply to the part
+which is the covered work, but the work with which it is combined will remain
+governed by version 3 of the GNU General Public License.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the
+GNU Affero General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to address
+new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+that a certain numbered version of the GNU Affero General Public License "or
+any later version" applies to it, you have the option of following the terms
+and conditions either of that numbered version or of any later version published
+by the Free Software Foundation. If the Program does not specify a version
+number of the GNU Affero General Public License, you may choose any version
+ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of
+the GNU Affero General Public License can be used, that proxy's public statement
+of acceptance of a version permanently authorizes you to choose that version
+for the Program.
+
+Later license versions may give you additional or different permissions.
+However, no additional obligations are imposed on any author or copyright
+holder as a result of your choosing to follow a later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
+LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK
+AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR
+OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
+ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
+AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
+USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot
+be given local legal effect according to their terms, reviewing courts shall
+apply local law that most closely approximates an absolute waiver of all civil
+liability in connection with the Program, unless a warranty or assumption
+of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible
+use to the public, the best way to achieve this is to make it free software
+which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach
+them to the start of each source file to most effectively state the exclusion
+of warranty; and each file should have at least the "copyright" line and a
+pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option)
+any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+details.
+
+You should have received a copy of the GNU Affero General Public License along
+with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If your software can interact with users remotely through a computer network,
+you should also make sure that it provides a way for users to get its source.
+For example, if your program is a web application, its interface could display
+a "Source" link that leads users to an archive of the code. There are many
+ways you could offer source, and different solutions will be better for different
+programs; see section 13 for the specific requirements.
+
+You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary. For
+more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
diff --git a/Dockerfile b/Dockerfile
index 3196ad0..316d274 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,60 @@
-FROM debian:stretch-slim
+FROM debian:buster-slim as files
+
+ARG travelynx_version=git
+
+COPY docker-run.sh /app/
+COPY index.pl /app/
+COPY lib/ /app/lib/
+COPY public/ /app/public/
+COPY templates/ /app/templates/
+COPY share/ /app/share/
+
+WORKDIR /app
+
+RUN ln -sf ../local/imprint.html.ep templates && \
+ ln -sf ../local/privacy.html.ep templates && \
+ ln -sf ../local/travelynx.conf
+
+RUN sed -i "s/qx{git describe --dirty}/'${travelynx_version}'/" lib/Travelynx/Controller/Static.pm
+RUN sed -i "s/\$self->plugin('Config');/\$self->plugin('Config'); \$self->config->{version} = '${travelynx_version}';/" lib/Travelynx.pm
+
+FROM perl:5.30-slim
ARG DEBIAN_FRONTEND=noninteractive
+ARG APT_LISTCHANGES_FRONTEND=none
-COPY cpanfile /app/cpanfile
+COPY cpanfile* /app/
WORKDIR /app
RUN apt-get update && apt-get install --no-install-recommends -y \
+ ca-certificates \
cpanminus \
- build-essential \
- libpq-dev \
+ gcc \
git \
+ libc6-dev \
+ libdb5.3 \
+ libdb5.3-dev \
+ libpq-dev \
+ libssl1.1 \
+ libssl-dev \
+ make \
+ zlib1g-dev \
&& cpanm -in --no-man-pages --installdeps . \
&& rm -rf ~/.cpanm \
&& apt-get purge -y \
- build-essential \
cpanminus \
- && apt-get autoremove -y
+ curl \
+ gcc \
+ libc6-dev \
+ libdb5.3-dev \
+ libssl-dev \
+ make \
+ zlib1g-dev \
+ && apt-get autoremove -y \
+ && rm -rf /var/cache/apt/* /var/lib/apt/lists/*
+
+COPY --from=files /app/ /app/
-COPY . /app
+EXPOSE 8093
-CMD ["/app/docker-run.sh"]
+ENTRYPOINT ["/app/docker-run.sh"]
diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt
new file mode 100644
index 0000000..3e275d8
--- /dev/null
+++ b/LICENSES/AGPL-3.0-or-later.txt
@@ -0,0 +1,603 @@
+GNU AFFERO GENERAL PUBLIC LICENSE
+Version 3, 19 November 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+ Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software
+and other kinds of works, specifically designed to ensure cooperation with
+the community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take
+away your freedom to share and change the works. By contrast, our General
+Public Licenses are intended to guarantee your freedom to share and change
+all versions of a program--to make sure it remains free software for all its
+users.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for them if you wish), that
+you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs, and that you know you
+can do these things.
+
+Developers that use our General Public Licenses protect your rights with two
+steps: (1) assert copyright on the software, and (2) offer you this License
+which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made
+in alternate versions of the program, if they receive widespread use, become
+available for other developers to incorporate. Many developers of free software
+are heartened and encouraged by the resulting cooperation. However, in the
+case of software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and letting
+the public access it on a server without ever releasing its source code to
+the public.
+
+The GNU Affero General Public License is designed specifically to ensure that,
+in such cases, the modified source code becomes available to the community.
+It requires the operator of a network server to provide the source code of
+the modified version running there to the users of that server. Therefore,
+public use of a modified version, on a publicly accessible server, gives the
+public access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by
+Affero, was designed to accomplish similar goals. This is a different license,
+not a version of the Affero GPL, but Affero has released a new version of
+the Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+ TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works,
+such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License.
+Each licensee is addressed as "you". "Licensees" and "recipients" may be
+individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in
+a fashion requiring copyright permission, other than the making of an exact
+copy. The resulting work is called a "modified version" of the earlier work
+or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the
+Program.
+
+To "propagate" a work means to do anything with it that, without permission,
+would make you directly or secondarily liable for infringement under applicable
+copyright law, except executing it on a computer or modifying a private copy.
+Propagation includes copying, distribution (with or without modification),
+making available to the public, and in some countries other activities as
+well.
+
+To "convey" a work means any kind of propagation that enables other parties
+to make or receive copies. Mere interaction with a user through a computer
+network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the
+extent that it includes a convenient and prominently visible feature that
+(1) displays an appropriate copyright notice, and (2) tells the user that
+there is no warranty for the work (except to the extent that warranties are
+provided), that licensees may convey the work under this License, and how
+to view a copy of this License. If the interface presents a list of user
+commands or options, such as a menu, a prominent item in the list meets this
+criterion.
+
+1. Source Code.
+The "source code" for a work means the preferred form of the work for making
+modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard
+defined by a recognized standards body, or, in the case of interfaces specified
+for a particular programming language, one that is widely used among developers
+working in that language.
+
+The "System Libraries" of an executable work include anything, other than
+the work as a whole, that (a) is included in the normal form of packaging
+a Major Component, but which is not part of that Major Component, and (b)
+serves only to enable use of the work with that Major Component, or to implement
+a Standard Interface for which an implementation is available to the public
+in source code form. A "Major Component", in this context, means a major
+essential component (kernel, window system, and so on) of the specific operating
+system (if any) on which the executable work runs, or a compiler used to produce
+the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source
+code needed to generate, install, and (for an executable work) run the object
+code and to modify the work, including scripts to control those activities.
+However, it does not include the work's System Libraries, or general-purpose
+tools or generally available free programs which are used unmodified in performing
+those activities but which are not part of the work. For example, Corresponding
+Source includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically linked
+subprograms that the work is specifically designed to require, such as by
+intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate
+automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+2. Basic Permissions.
+All rights granted under this License are granted for the term of copyright
+on the Program, and are irrevocable provided the stated conditions are met.
+This License explicitly affirms your unlimited permission to run the unmodified
+Program. The output from running a covered work is covered by this License
+only if the output, given its content, constitutes a covered work. This License
+acknowledges your rights of fair use or other equivalent, as provided by copyright
+law.
+
+You may make, run and propagate covered works that you do not convey, without
+conditions so long as your license otherwise remains in force. You may convey
+covered works to others for the sole purpose of having them make modifications
+exclusively for you, or provide you with facilities for running those works,
+provided that you comply with the terms of this License in conveying all material
+for which you do not control copyright. Those thus making or running the
+covered works for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of your copyrighted
+material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions
+stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+No covered work shall be deemed part of an effective technological measure
+under any applicable law fulfilling obligations under article 11 of the WIPO
+copyright treaty adopted on 20 December 1996, or similar laws prohibiting
+or restricting circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention
+of technological measures to the extent such circumvention is effected by
+exercising rights under this License with respect to the covered work, and
+you disclaim any intention to limit operation or modification of the work
+as a means of enforcing, against the work's users, your or third parties'
+legal rights to forbid circumvention of technological measures.
+
+4. Conveying Verbatim Copies.
+You may convey verbatim copies of the Program's source code as you receive
+it, in any medium, provided that you conspicuously and appropriately publish
+on each copy an appropriate copyright notice; keep intact all notices stating
+that this License and any non-permissive terms added in accord with section
+7 apply to the code; keep intact all notices of the absence of any warranty;
+and give all recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you
+may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+You may convey a work based on the Program, or the modifications to produce
+it from the Program, in the form of source code under the terms of section
+4, provided that you also meet all of these conditions:
+
+a) The work must carry prominent notices stating that you modified it, and
+giving a relevant date.
+
+b) The work must carry prominent notices stating that it is released under
+this License and any conditions added under section 7. This requirement modifies
+the requirement in section 4 to "keep intact all notices".
+
+c) You must license the entire work, as a whole, under this License to anyone
+who comes into possession of a copy. This License will therefore apply, along
+with any applicable section 7 additional terms, to the whole of the work,
+and all its parts, regardless of how they are packaged. This License gives
+no permission to license the work in any other way, but it does not invalidate
+such permission if you have separately received it.
+
+d) If the work has interactive user interfaces, each must display Appropriate
+Legal Notices; however, if the Program has interactive interfaces that do
+not display Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works,
+which are not by their nature extensions of the covered work, and which are
+not combined with it such as to form a larger program, in or on a volume of
+a storage or distribution medium, is called an "aggregate" if the compilation
+and its resulting copyright are not used to limit the access or legal rights
+of the compilation's users beyond what the individual works permit. Inclusion
+of a covered work in an aggregate does not cause this License to apply to
+the other parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+You may convey a covered work in object code form under the terms of sections
+4 and 5, provided that you also convey the machine-readable Corresponding
+Source under the terms of this License, in one of these ways:
+
+a) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by the Corresponding Source fixed
+on a durable physical medium customarily used for software interchange.
+
+b) Convey the object code in, or embodied in, a physical product (including
+a physical distribution medium), accompanied by a written offer, valid for
+at least three years and valid for as long as you offer spare parts or customer
+support for that product model, to give anyone who possesses the object code
+either (1) a copy of the Corresponding Source for all the software in the
+product that is covered by this License, on a durable physical medium customarily
+used for software interchange, for a price no more than your reasonable cost
+of physically performing this conveying of source, or (2) access to copy the
+Corresponding Source from a network server at no charge.
+
+c) Convey individual copies of the object code with a copy of the written
+offer to provide the Corresponding Source. This alternative is allowed only
+occasionally and noncommercially, and only if you received the object code
+with such an offer, in accord with subsection 6b.
+
+d) Convey the object code by offering access from a designated place (gratis
+or for a charge), and offer equivalent access to the Corresponding Source
+in the same way through the same place at no further charge. You need not
+require recipients to copy the Corresponding Source along with the object
+code. If the place to copy the object code is a network server, the Corresponding
+Source may be on a different server (operated by you or a third party) that
+supports equivalent copying facilities, provided you maintain clear directions
+next to the object code saying where to find the Corresponding Source. Regardless
+of what server hosts the Corresponding Source, you remain obligated to ensure
+that it is available for as long as needed to satisfy these requirements.
+
+e) Convey the object code using peer-to-peer transmission, provided you inform
+other peers where the object code and Corresponding Source of the work are
+being offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from
+the Corresponding Source as a System Library, need not be included in conveying
+the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible
+personal property which is normally used for personal, family, or household
+purposes, or (2) anything designed or sold for incorporation into a dwelling.
+In determining whether a product is a consumer product, doubtful cases shall
+be resolved in favor of coverage. For a particular product received by a
+particular user, "normally used" refers to a typical or common use of that
+class of product, regardless of the status of the particular user or of the
+way in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of whether
+the product has substantial commercial, industrial or non-consumer uses, unless
+such uses represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures,
+authorization keys, or other information required to install and execute modified
+versions of a covered work in that User Product from a modified version of
+its Corresponding Source. The information must suffice to ensure that the
+continued functioning of the modified object code is in no case prevented
+or interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically
+for use in, a User Product, and the conveying occurs as part of a transaction
+in which the right of possession and use of the User Product is transferred
+to the recipient in perpetuity or for a fixed term (regardless of how the
+transaction is characterized), the Corresponding Source conveyed under this
+section must be accompanied by the Installation Information. But this requirement
+does not apply if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has been installed
+in ROM).
+
+The requirement to provide Installation Information does not include a requirement
+to continue to provide support service, warranty, or updates for a work that
+has been modified or installed by the recipient, or for the User Product in
+which it has been modified or installed. Access to a network may be denied
+when the modification itself materially and adversely affects the operation
+of the network or violates the rules and protocols for communication across
+the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord
+with this section must be in a format that is publicly documented (and with
+an implementation available to the public in source code form), and must require
+no special password or key for unpacking, reading or copying.
+
+7. Additional Terms.
+"Additional permissions" are terms that supplement the terms of this License
+by making exceptions from one or more of its conditions. Additional permissions
+that are applicable to the entire Program shall be treated as though they
+were included in this License, to the extent that they are valid under applicable
+law. If additional permissions apply only to part of the Program, that part
+may be used separately under those permissions, but the entire Program remains
+governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any
+additional permissions from that copy, or from any part of it. (Additional
+permissions may be written to require their own removal in certain cases when
+you modify the work.) You may place additional permissions on material, added
+by you to a covered work, for which you have or can give appropriate copyright
+permission.
+
+Notwithstanding any other provision of this License, for material you add
+to a covered work, you may (if authorized by the copyright holders of that
+material) supplement the terms of this License with terms:
+
+a) Disclaiming warranty or limiting liability differently from the terms of
+sections 15 and 16 of this License; or
+
+b) Requiring preservation of specified reasonable legal notices or author
+attributions in that material or in the Appropriate Legal Notices displayed
+by works containing it; or
+
+c) Prohibiting misrepresentation of the origin of that material, or requiring
+that modified versions of such material be marked in reasonable ways as different
+from the original version; or
+
+d) Limiting the use for publicity purposes of names of licensors or authors
+of the material; or
+
+e) Declining to grant rights under trademark law for use of some trade names,
+trademarks, or service marks; or
+
+f) Requiring indemnification of licensors and authors of that material by
+anyone who conveys the material (or modified versions of it) with contractual
+assumptions of liability to the recipient, for any liability that these contractual
+assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions"
+within the meaning of section 10. If the Program as you received it, or any
+part of it, contains a notice stating that it is governed by this License
+along with a term that is a further restriction, you may remove that term.
+If a license document contains a further restriction but permits relicensing
+or conveying under this License, you may add to a covered work material governed
+by the terms of that license document, provided that the further restriction
+does not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place,
+in the relevant source files, a statement of the additional terms that apply
+to those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form
+of a separately written license, or stated as exceptions; the above requirements
+apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided
+under this License. Any attempt otherwise to propagate or modify it is void,
+and will automatically terminate your rights under this License (including
+any patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from
+a particular copyright holder is reinstated (a) provisionally, unless and
+until the copyright holder explicitly and finally terminates your license,
+and (b) permanently, if the copyright holder fails to notify you of the violation
+by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently
+if the copyright holder notifies you of the violation by some reasonable means,
+this is the first time you have received notice of violation of this License
+(for any work) from that copyright holder, and you cure the violation prior
+to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses
+of parties who have received copies or rights from you under this License.
+If your rights have been terminated and not permanently reinstated, you do
+not qualify to receive new licenses for the same material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy
+of the Program. Ancillary propagation of a covered work occurring solely
+as a consequence of using peer-to-peer transmission to receive a copy likewise
+does not require acceptance. However, nothing other than this License grants
+you permission to propagate or modify any covered work. These actions infringe
+copyright if you do not accept this License. Therefore, by modifying or propagating
+a covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives
+a license from the original licensors, to run, modify and propagate that work,
+subject to this License. You are not responsible for enforcing compliance
+by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an organization,
+or substantially all assets of one, or subdividing an organization, or merging
+organizations. If propagation of a covered work results from an entity transaction,
+each party to that transaction who receives a copy of the work also receives
+whatever licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the Corresponding
+Source of the work from the predecessor in interest, if the predecessor has
+it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights
+granted or affirmed under this License. For example, you may not impose a
+license fee, royalty, or other charge for exercise of rights granted under
+this License, and you may not initiate litigation (including a cross-claim
+or counterclaim in a lawsuit) alleging that any patent claim is infringed
+by making, using, selling, offering for sale, or importing the Program or
+any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License
+of the Program or a work on which the Program is based. The work thus licensed
+is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or controlled
+by the contributor, whether already acquired or hereafter acquired, that would
+be infringed by some manner, permitted by this License, of making, using,
+or selling its contributor version, but do not include claims that would be
+infringed only as a consequence of further modification of the contributor
+version. For purposes of this definition, "control" includes the right to
+grant patent sublicenses in a manner consistent with the requirements of this
+License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent
+license under the contributor's essential patent claims, to make, use, sell,
+offer for sale, import and otherwise run, modify and propagate the contents
+of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement
+or commitment, however denominated, not to enforce a patent (such as an express
+permission to practice a patent or covenant not to sue for patent infringement).
+To "grant" such a patent license to a party means to make such an agreement
+or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the
+Corresponding Source of the work is not available for anyone to copy, free
+of charge and under the terms of this License, through a publicly available
+network server or other readily accessible means, then you must either (1)
+cause the Corresponding Source to be so available, or (2) arrange to deprive
+yourself of the benefit of the patent license for this particular work, or
+(3) arrange, in a manner consistent with the requirements of this License,
+to extend the patent
+license to downstream recipients. "Knowingly relying" means you have actual
+knowledge that, but for the patent license, your conveying the covered work
+in a country, or your recipient's use of the covered work in a country, would
+infringe one or more identifiable patents in that country that you have reason
+to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement,
+you convey, or propagate by procuring conveyance of, a covered work, and grant
+a patent license to some of the parties receiving the covered work authorizing
+them to use, propagate, modify or convey a specific copy of the covered work,
+then the patent license you grant is automatically extended to all recipients
+of the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope
+of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
+of one or more of the rights that are specifically granted under this License.
+You may not convey a covered work if you are a party to an arrangement with
+a third party that is in the business of distributing software, under which
+you make payment to the third party based on the extent of your activity of
+conveying the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by you
+(or copies made from those copies), or (b) primarily for and in connection
+with specific products or compilations that contain the covered work, unless
+you entered into that arrangement, or that patent license was granted, prior
+to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied
+license or other defenses to infringement that may otherwise be available
+to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise)
+that contradict the conditions of this License, they do not excuse you from
+the conditions of this License. If you cannot convey a covered work so as
+to satisfy simultaneously your obligations under this License and any other
+pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey the
+Program, the only way you could satisfy both those terms and this License
+would be to refrain entirely from conveying the Program.
+
+13. Remote Network Interaction; Use with the GNU General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program,
+your modified version must prominently offer all users interacting with it
+remotely through a computer network (if your version supports such interaction)
+an opportunity to receive the Corresponding Source of your version by providing
+access to the Corresponding Source from a network server at no charge, through
+some standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any work covered
+by version 3 of the GNU General Public License that is incorporated pursuant
+to the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to
+link or combine any covered work with a work licensed under version 3 of the
+GNU General Public License into a single combined work, and to convey the
+resulting work. The terms of this License will continue to apply to the part
+which is the covered work, but the work with which it is combined will remain
+governed by version 3 of the GNU General Public License.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the
+GNU Affero General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to address
+new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+that a certain numbered version of the GNU Affero General Public License "or
+any later version" applies to it, you have the option of following the terms
+and conditions either of that numbered version or of any later version published
+by the Free Software Foundation. If the Program does not specify a version
+number of the GNU Affero General Public License, you may choose any version
+ever published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of
+the GNU Affero General Public License can be used, that proxy's public statement
+of acceptance of a version permanently authorizes you to choose that version
+for the Program.
+
+Later license versions may give you additional or different permissions.
+However, no additional obligations are imposed on any author or copyright
+holder as a result of your choosing to follow a later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
+LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK
+AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR
+OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
+ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
+AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
+USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot
+be given local legal effect according to their terms, reviewing courts shall
+apply local law that most closely approximates an absolute waiver of all civil
+liability in connection with the Program, unless a warranty or assumption
+of liability accompanies a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible
+use to the public, the best way to achieve this is to make it free software
+which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach
+them to the start of each source file to most effectively state the exclusion
+of warranty; and each file should have at least the "copyright" line and a
+pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option)
+any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+details.
+
+You should have received a copy of the GNU Affero General Public License along
+with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If your software can interact with users remotely through a computer network,
+you should also make sure that it provides a way for users to get its source.
+For example, if your program is a web application, its interface could display
+a "Source" link that leads users to an archive of the code. There are many
+ways you could offer source, and different solutions will be better for different
+programs; see section 13 for the specific requirements.
+
+You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary. For
+more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
new file mode 100644
index 0000000..9a4104b
--- /dev/null
+++ b/LICENSES/Apache-2.0.txt
@@ -0,0 +1,183 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution
+as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct
+or indirect, to cause the direction or management of such entity, whether
+by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
+of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
+granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation
+or translation of a Source form, including but not limited to compiled object
+code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form,
+made available under the License, as indicated by a copyright notice that
+is included in or attached to the work (an example is provided in the Appendix
+below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form,
+that is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative
+Works shall not include works that remain separable from, or merely link (or
+bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative
+Works thereof, that is intentionally submitted to Licensor for inclusion in
+the Work by the copyright owner or by an individual or Legal Entity authorized
+to submit on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication
+sent to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor
+for the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently incorporated
+within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this
+License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
+Derivative Works of, publicly display, publicly perform, sublicense, and distribute
+the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License,
+each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section) patent
+license to make, have made, use, offer to sell, sell, import, and otherwise
+transfer the Work, where such license applies only to those patent claims
+licensable by such Contributor that are necessarily infringed by their Contribution(s)
+alone or by combination of their Contribution(s) with the Work to which such
+Contribution(s) was submitted. If You institute patent litigation against
+any entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that the Work or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses granted to You
+under this License for that Work shall terminate as of the date such litigation
+is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or
+Derivative Works thereof in any medium, with or without modifications, and
+in Source or Object form, provided that You meet the following conditions:
+
+(a) You must give any other recipients of the Work or Derivative Works a copy
+of this License; and
+
+(b) You must cause any modified files to carry prominent notices stating that
+You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source
+form of the Work, excluding those notices that do not pertain to any part
+of the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its distribution,
+then any Derivative Works that You distribute must include a readable copy
+of the attribution notices contained within such NOTICE file, excluding those
+notices that do not pertain to any part of the Derivative Works, in at least
+one of the following places: within a NOTICE text file distributed as part
+of the Derivative Works; within the Source form or documentation, if provided
+along with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works
+that You distribute, alongside or as an addendum to the NOTICE text from the
+Work, provided that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction,
+or distribution of Your modifications, or for any such Derivative Works as
+a whole, provided Your use, reproduction, and distribution of the Work otherwise
+complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any
+Contribution intentionally submitted for inclusion in the Work by You to the
+Licensor shall be under the terms and conditions of this License, without
+any additional terms or conditions. Notwithstanding the above, nothing herein
+shall supersede or modify the terms of any separate license agreement you
+may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except as required
+for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to
+in writing, Licensor provides the Work (and each Contributor provides its
+Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied, including, without limitation, any warranties
+or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
+A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
+of using or redistributing the Work and assume any risks associated with Your
+exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether
+in tort (including negligence), contract, or otherwise, unless required by
+applicable law (such as deliberate and grossly negligent acts) or agreed to
+in writing, shall any Contributor be liable to You for damages, including
+any direct, indirect, special, incidental, or consequential damages of any
+character arising as a result of this License or out of the use or inability
+to use the Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all other commercial
+damages or losses), even if such Contributor has been advised of the possibility
+of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work
+or Derivative Works thereof, You may choose to offer, and charge a fee for,
+acceptance of support, warranty, indemnity, or other liability obligations
+and/or rights consistent with this License. However, in accepting such obligations,
+You may act only on Your own behalf and on Your sole responsibility, not on
+behalf of any other Contributor, and only if You agree to indemnify, defend,
+and hold each Contributor harmless for any liability incurred by, or claims
+asserted against, such Contributor by reason of your accepting any such warranty
+or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own identifying
+information. (Don't include the brackets!) The text should be enclosed in
+the appropriate comment syntax for the file format. We also recommend that
+a file or class name and description of purpose be included on the same "printed
+page" as the copyright notice for easier identification within third-party
+archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt
new file mode 100644
index 0000000..baa80b5
--- /dev/null
+++ b/LICENSES/BSD-2-Clause.txt
@@ -0,0 +1,22 @@
+Copyright (c) <year> <owner> All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt
new file mode 100644
index 0000000..3d6abd8
--- /dev/null
+++ b/LICENSES/CC-BY-4.0.txt
@@ -0,0 +1,324 @@
+Creative Commons Attribution 4.0 International
+
+Creative Commons Corporation (“Creative Commons”) is not a law firm and does
+not provide legal services or legal advice. Distribution of Creative Commons
+public licenses does not create a lawyer-client or other relationship. Creative
+Commons makes its licenses and related information available on an “as-is”
+basis. Creative Commons gives no warranties regarding its licenses, any material
+licensed under their terms and conditions, or any related information. Creative
+Commons disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and conditions
+that creators and other rights holders may use to share original works of
+authorship and other material subject to copyright and certain other rights
+specified in the public license below. The following considerations are for
+informational purposes only, are not exhaustive, and do not form part of our
+licenses.
+
+Considerations for licensors: Our public licenses are intended for use by
+those authorized to give the public permission to use material in ways otherwise
+restricted by copyright and certain other rights. Our licenses are irrevocable.
+Licensors should read and understand the terms and conditions of the license
+they choose before applying it. Licensors should also secure all rights necessary
+before applying our licenses so that the public can reuse the material as
+expected. Licensors should clearly mark any material not subject to the license.
+This includes other CC-licensed material, or material used under an exception
+or limitation to copyright. More considerations for licensors.
+
+Considerations for the public: By using one of our public licenses, a licensor
+grants the public permission to use the licensed material under specified
+terms and conditions. If the licensor’s permission is not necessary for any
+reason–for example, because of any applicable exception or limitation to copyright–then
+that use is not regulated by the license. Our licenses grant only permissions
+under copyright and certain other rights that a licensor has authority to
+grant. Use of the licensed material may still be restricted for other reasons,
+including because others have copyright or other rights in the material. A
+licensor may make special requests, such as asking that all changes be marked
+or described. Although not required by our licenses, you are encouraged to
+respect those requests where reasonable. More considerations for the public.
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree to
+be bound by the terms and conditions of this Creative Commons Attribution
+4.0 International Public License ("Public License"). To the extent this Public
+License may be interpreted as a contract, You are granted the Licensed Rights
+in consideration of Your acceptance of these terms and conditions, and the
+Licensor grants You such rights in consideration of benefits the Licensor
+receives from making the Licensed Material available under these terms and
+conditions.
+
+Section 1 – Definitions.
+
+a. Adapted Material means material subject to Copyright and Similar Rights
+that is derived from or based upon the Licensed Material and in which the
+Licensed Material is translated, altered, arranged, transformed, or otherwise
+modified in a manner requiring permission under the Copyright and Similar
+Rights held by the Licensor. For purposes of this Public License, where the
+Licensed Material is a musical work, performance, or sound recording, Adapted
+Material is always produced where the Licensed Material is synched in timed
+relation with a moving image.
+
+b. Adapter's License means the license You apply to Your Copyright and Similar
+Rights in Your contributions to Adapted Material in accordance with the terms
+and conditions of this Public License.
+
+c. Copyright and Similar Rights means copyright and/or similar rights closely
+related to copyright including, without limitation, performance, broadcast,
+sound recording, and Sui Generis Database Rights, without regard to how the
+rights are labeled or categorized. For purposes of this Public License, the
+rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
+
+d. Effective Technological Measures means those measures that, in the absence
+of proper authority, may not be circumvented under laws fulfilling obligations
+under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996,
+and/or similar international agreements.
+
+e. Exceptions and Limitations means fair use, fair dealing, and/or any other
+exception or limitation to Copyright and Similar Rights that applies to Your
+use of the Licensed Material.
+
+f. Licensed Material means the artistic or literary work, database, or other
+material to which the Licensor applied this Public License.
+
+g. Licensed Rights means the rights granted to You subject to the terms and
+conditions of this Public License, which are limited to all Copyright and
+Similar Rights that apply to Your use of the Licensed Material and that the
+Licensor has authority to license.
+
+h. Licensor means the individual(s) or entity(ies) granting rights under this
+Public License.
+
+i. Share means to provide material to the public by any means or process that
+requires permission under the Licensed Rights, such as reproduction, public
+display, public performance, distribution, dissemination, communication, or
+importation, and to make material available to the public including in ways
+that members of the public may access the material from a place and at a time
+individually chosen by them.
+
+j. Sui Generis Database Rights means rights other than copyright resulting
+from Directive 96/9/EC of the European Parliament and of the Council of 11
+March 1996 on the legal protection of databases, as amended and/or succeeded,
+as well as other essentially equivalent rights anywhere in the world.
+
+k. You means the individual or entity exercising the Licensed Rights under
+this Public License. Your has a corresponding meaning.
+
+Section 2 – Scope.
+
+ a. License grant.
+
+1. Subject to the terms and conditions of this Public License, the Licensor
+hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive,
+irrevocable license to exercise the Licensed Rights in the Licensed Material
+to:
+
+A. reproduce and Share the Licensed Material, in whole or in part; and
+
+ B. produce, reproduce, and Share Adapted Material.
+
+2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions
+and Limitations apply to Your use, this Public License does not apply, and
+You do not need to comply with its terms and conditions.
+
+3. Term. The term of this Public License is specified in Section 6(a).
+
+4. Media and formats; technical modifications allowed. The Licensor authorizes
+You to exercise the Licensed Rights in all media and formats whether now known
+or hereafter created, and to make technical modifications necessary to do
+so. The Licensor waives and/or agrees not to assert any right or authority
+to forbid You from making technical modifications necessary to exercise the
+Licensed Rights, including technical modifications necessary to circumvent
+Effective Technological Measures. For purposes of this Public License, simply
+making modifications authorized by this Section 2(a)(4) never produces Adapted
+Material.
+
+ 5. Downstream recipients.
+
+A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed
+Material automatically receives an offer from the Licensor to exercise the
+Licensed Rights under the terms and conditions of this Public License.
+
+B. No downstream restrictions. You may not offer or impose any additional
+or different terms or conditions on, or apply any Effective Technological
+Measures to, the Licensed Material if doing so restricts exercise of the Licensed
+Rights by any recipient of the Licensed Material.
+
+6. No endorsement. Nothing in this Public License constitutes or may be construed
+as permission to assert or imply that You are, or that Your use of the Licensed
+Material is, connected with, or sponsored, endorsed, or granted official status
+by, the Licensor or others designated to receive attribution as provided in
+Section 3(a)(1)(A)(i).
+
+b. Other rights.
+
+1. Moral rights, such as the right of integrity, are not licensed under this
+Public License, nor are publicity, privacy, and/or other similar personality
+rights; however, to the extent possible, the Licensor waives and/or agrees
+not to assert any such rights held by the Licensor to the limited extent necessary
+to allow You to exercise the Licensed Rights, but not otherwise.
+
+2. Patent and trademark rights are not licensed under this Public License.
+
+3. To the extent possible, the Licensor waives any right to collect royalties
+from You for the exercise of the Licensed Rights, whether directly or through
+a collecting society under any voluntary or waivable statutory or compulsory
+licensing scheme. In all other cases the Licensor expressly reserves any right
+to collect such royalties.
+
+Section 3 – License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the following
+conditions.
+
+ a. Attribution.
+
+1. If You Share the Licensed Material (including in modified form), You must:
+
+A. retain the following if it is supplied by the Licensor with the Licensed
+Material:
+
+i. identification of the creator(s) of the Licensed Material and any others
+designated to receive attribution, in any reasonable manner requested by the
+Licensor (including by pseudonym if designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of warranties;
+
+v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
+
+B. indicate if You modified the Licensed Material and retain an indication
+of any previous modifications; and
+
+C. indicate the Licensed Material is licensed under this Public License, and
+include the text of, or the URI or hyperlink to, this Public License.
+
+2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner
+based on the medium, means, and context in which You Share the Licensed Material.
+For example, it may be reasonable to satisfy the conditions by providing a
+URI or hyperlink to a resource that includes the required information.
+
+3. If requested by the Licensor, You must remove any of the information required
+by Section 3(a)(1)(A) to the extent reasonably practicable.
+
+4. If You Share Adapted Material You produce, the Adapter's License You apply
+must not prevent recipients of the Adapted Material from complying with this
+Public License.
+
+Section 4 – Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that apply to
+Your use of the Licensed Material:
+
+a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract,
+reuse, reproduce, and Share all or a substantial portion of the contents of
+the database;
+
+b. if You include all or a substantial portion of the database contents in
+a database in which You have Sui Generis Database Rights, then the database
+in which You have Sui Generis Database Rights (but not its individual contents)
+is Adapted Material; and
+
+c. You must comply with the conditions in Section 3(a) if You Share all or
+a substantial portion of the contents of the database.
+For the avoidance of doubt, this Section 4 supplements and does not replace
+Your obligations under this Public License where the Licensed Rights include
+other Copyright and Similar Rights.
+
+Section 5 – Disclaimer of Warranties and Limitation of Liability.
+
+a. Unless otherwise separately undertaken by the Licensor, to the extent possible,
+the Licensor offers the Licensed Material as-is and as-available, and makes
+no representations or warranties of any kind concerning the Licensed Material,
+whether express, implied, statutory, or other. This includes, without limitation,
+warranties of title, merchantability, fitness for a particular purpose, non-infringement,
+absence of latent or other defects, accuracy, or the presence or absence of
+errors, whether or not known or discoverable. Where disclaimers of warranties
+are not allowed in full or in part, this disclaimer may not apply to You.
+
+b. To the extent possible, in no event will the Licensor be liable to You
+on any legal theory (including, without limitation, negligence) or otherwise
+for any direct, special, indirect, incidental, consequential, punitive, exemplary,
+or other losses, costs, expenses, or damages arising out of this Public License
+or use of the Licensed Material, even if the Licensor has been advised of
+the possibility of such losses, costs, expenses, or damages. Where a limitation
+of liability is not allowed in full or in part, this limitation may not apply
+to You.
+
+c. The disclaimer of warranties and limitation of liability provided above
+shall be interpreted in a manner that, to the extent possible, most closely
+approximates an absolute disclaimer and waiver of all liability.
+
+Section 6 – Term and Termination.
+
+a. This Public License applies for the term of the Copyright and Similar Rights
+licensed here. However, if You fail to comply with this Public License, then
+Your rights under this Public License terminate automatically.
+
+b. Where Your right to use the Licensed Material has terminated under Section
+6(a), it reinstates:
+
+1. automatically as of the date the violation is cured, provided it is cured
+within 30 days of Your discovery of the violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+c. For the avoidance of doubt, this Section 6(b) does not affect any right
+the Licensor may have to seek remedies for Your violations of this Public
+License.
+
+d. For the avoidance of doubt, the Licensor may also offer the Licensed Material
+under separate terms or conditions or stop distributing the Licensed Material
+at any time; however, doing so will not terminate this Public License.
+
+ e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
+
+Section 7 – Other Terms and Conditions.
+
+a. The Licensor shall not be bound by any additional or different terms or
+conditions communicated by You unless expressly agreed.
+
+b. Any arrangements, understandings, or agreements regarding the Licensed
+Material not stated herein are separate from and independent of the terms
+and conditions of this Public License.
+
+Section 8 – Interpretation.
+
+a. For the avoidance of doubt, this Public License does not, and shall not
+be interpreted to, reduce, limit, restrict, or impose conditions on any use
+of the Licensed Material that could lawfully be made without permission under
+this Public License.
+
+b. To the extent possible, if any provision of this Public License is deemed
+unenforceable, it shall be automatically reformed to the minimum extent necessary
+to make it enforceable. If the provision cannot be reformed, it shall be severed
+from this Public License without affecting the enforceability of the remaining
+terms and conditions.
+
+c. No term or condition of this Public License will be waived and no failure
+to comply consented to unless expressly agreed to by the Licensor.
+
+d. Nothing in this Public License constitutes or may be interpreted as a limitation
+upon, or waiver of, any privileges and immunities that apply to the Licensor
+or You, including from the legal processes of any jurisdiction or authority.
+
+Creative Commons is not a party to its public licenses. Notwithstanding, Creative
+Commons may elect to apply one of its public licenses to material it publishes
+and in those instances will be considered the “Licensor.” Except for the limited
+purpose of indicating that material is shared under a Creative Commons public
+license or as otherwise permitted by the Creative Commons policies published
+at creativecommons.org/policies, Creative Commons does not authorize the use
+of the trademark “Creative Commons” or any other trademark or logo of Creative
+Commons without its prior written consent including, without limitation, in
+connection with any unauthorized modifications to any of its public licenses
+or any other arrangements, understandings, or agreements concerning use of
+licensed material. For the avoidance of doubt, this paragraph does not form
+part of the public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644
index 0000000..b1f1cd2
--- /dev/null
+++ b/LICENSES/CC0-1.0.txt
@@ -0,0 +1,1520 @@
+
+
+
+
+
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <link rel="dns-prefetch" href="https://github.githubassets.com">
+ <link rel="dns-prefetch" href="https://avatars0.githubusercontent.com">
+ <link rel="dns-prefetch" href="https://avatars1.githubusercontent.com">
+ <link rel="dns-prefetch" href="https://avatars2.githubusercontent.com">
+ <link rel="dns-prefetch" href="https://avatars3.githubusercontent.com">
+ <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com">
+ <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/">
+
+
+
+ <link crossorigin="anonymous" media="all" integrity="sha512-2rYmZoKIqVHGi0fW7GJrP+qKbL5ErE1RNyshLtTKkUM9FrjqyVJhth2DBKFda7dk5rKkpyp76lFeZX6K8GJyxg==" rel="stylesheet" href="https://github.githubassets.com/assets/frameworks-dab626668288a951c68b47d6ec626b3f.css" />
+ <link crossorigin="anonymous" media="all" integrity="sha512-FAi7xqbQQfppKgT+dVH76S0lWoYgmuK/p8k/P62pGQXlKBCOqIEd5boWUkZwlpY2j7TFFI2lb3E14ErtDsAqgw==" rel="stylesheet" href="https://github.githubassets.com/assets/site-1408bbc6a6d041fa692a04fe7551fbe9.css" />
+ <link crossorigin="anonymous" media="all" integrity="sha512-HBILZgW5OMGedN0S3mMi049fYgDuNrnwO8cvmx6hY/VCHaJdGx20AfUcAsOPT6S/AH0Xda12tLSCNRKqc46Emg==" rel="stylesheet" href="https://github.githubassets.com/assets/github-1c120b6605b938c19e74dd12de6322d3.css" />
+
+
+
+
+
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-8K2vvwbW+6H27Nad5ydg8PA2/aMD/LKq+EiK9s0U0hhVZxCI2tWBsYk9beAtisRw2j+Or5k2/F+6dk02nmj/PA==" type="application/javascript" src="https://github.githubassets.com/assets/environment-f0adafbf.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-sR3f0kahoeblfszzL150uYKqur19dEEBZeYzKF+a1/qyBjcSruHGsXeE5HDlD6PTe2hyPJtuKXPbylp3lhZOOw==" type="application/javascript" src="https://github.githubassets.com/assets/chunk-frameworks-b11ddfd2.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-57BXsO8XaL0t9BZAtpvxNYFJHfZNrBwkfSyGeC3exSzN6O8TCruBADgWYbNEL8j3du298LBu6uVHMaO9l0NNIg==" type="application/javascript" src="https://github.githubassets.com/assets/chunk-vendor-e7b057b0.js"></script>
+
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-90s+EchqSGemmwvnqF1Q+zKyzmh6ujbmNUYNsDCo6v5L83KTNgxDFvSZiJYU1G0fM+ZP+lxDkmz08SvH+GCpCQ==" type="application/javascript" src="https://github.githubassets.com/assets/behaviors-f74b3e11.js"></script>
+
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-F/8sNCLF603gJL0egQeZwFTYP9dfZAVEWHdjMssZ2opLZXRK08vgowBRzKyra6u9zU/rGVxe+igfJsSWzfO2lg==" type="application/javascript" data-module-id="./chunk-contributions-spider-graph.js" data-src="https://github.githubassets.com/assets/chunk-contributions-spider-graph-17ff2c34.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-obMR8mPKx8OvqRe34LgnUcxeJ1qujiA4ND3H6UX13ExMlA/WfHLjEzXRmgGRcRvN/8J1nzc+Z+jgz/PLTFy6zg==" type="application/javascript" data-module-id="./chunk-drag-drop.js" data-src="https://github.githubassets.com/assets/chunk-drag-drop-a1b311f2.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-nrQUXHpCZwcjRbjr6hKa32vRLemL+gZ80QN2bCyp+57xvm0XjXTiz1YOUbb2N44Y2dveV3WmkBx0wvgs4t61LA==" type="application/javascript" data-module-id="./chunk-jump-to.js" data-src="https://github.githubassets.com/assets/chunk-jump-to-9eb4145c.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-tcH4xCRuMBAh1PruDaiwGnRIbHlF6bGLhxyCQ16uqok1cV5QFMguVPWJtN9KI0jGQOgN+Pha3+uOUXhXdfK/qw==" type="application/javascript" data-module-id="./chunk-profile-pins-element.js" data-src="https://github.githubassets.com/assets/chunk-profile-pins-element-b5c1f8c4.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-gPx3bYhTjyC83X5u5KlEDJpwGAHt3AC2p5s9iMuAfPTeSj7kHlKMW231C3K3c7+jvlpWpELk8DJsefrYdRzqjA==" type="application/javascript" data-module-id="./chunk-randomColor.js" data-src="https://github.githubassets.com/assets/chunk-randomColor-80fc776d.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-E+H+wAtjiqutBvn2cnXzDIvmasIhYiS7i7JzOfFUwo+Ej8zT54OrJtP//RhwixnypgOpCF4JvqzYy6zOtORDmg==" type="application/javascript" data-module-id="./chunk-runner-groups.js" data-src="https://github.githubassets.com/assets/chunk-runner-groups-13e1fec0.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-qdiCkJPPR4LxwUKftEmJe2v79E8xnTceYqylsWkMsGuARkiKkX9iFNwkvZJ3bDfS5YHSPD3+k+N2/I73tvlL1Q==" type="application/javascript" data-module-id="./chunk-sortable-behavior.js" data-src="https://github.githubassets.com/assets/chunk-sortable-behavior-a9d88290.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-NpURjBPyJ0JT8hWOMbLErYNeb0bTkKfmFX1hl1F8C/q6jckqWObeOzEAcs6TRlj+cqAR6GDohEBxDgkYBlx+QQ==" type="application/javascript" data-module-id="./chunk-tweetsodium.js" data-src="https://github.githubassets.com/assets/chunk-tweetsodium-3695118c.js"></script>
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-cEJZ+VgdZDdWnklo2jrp7veZDGa+XAZPkxbgGxeqSoJfiIFqQp2GVMUXQKHWXEmjJKwRd+HYqO1YWFR8bKP5/A==" type="application/javascript" data-module-id="./chunk-user-status-submit.js" data-src="https://github.githubassets.com/assets/chunk-user-status-submit-704259f9.js"></script>
+
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-4iEIux17YSDPKxmS8ITWA1zfrO2yZyuJ6ELxhHgk8BkytQc/lEfUDAjRztGxGGcScEkb0AO7dYaDRwsaDJHBRQ==" type="application/javascript" src="https://github.githubassets.com/assets/repositories-e22108bb.js"></script>
+<script crossorigin="anonymous" defer="defer" integrity="sha512-Z85KePfU4y7VBH+WD5JbWkuprlyDJwUtOULegAY7IVGpBfD+WP//G1zhHQarks3Z6WeR5E1Ee+qPJPix754UnQ==" type="application/javascript" src="https://github.githubassets.com/assets/diffs-67ce4a78.js"></script>
+
+ <meta name="viewport" content="width=device-width">
+
+ <title>license-list-data/CC0-1.0.txt at master · spdx/license-list-data · GitHub</title>
+ <meta name="description" content="Various data formats for the SPDX License List including RDFa, HTML, Text, and JSON - spdx/license-list-data">
+ <link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub">
+ <link rel="fluid-icon" href="https://github.com/fluidicon.png" title="GitHub">
+ <meta property="fb:app_id" content="1401488693436528">
+ <meta name="apple-itunes-app" content="app-id=1477376905" />
+ <meta name="twitter:image:src" content="https://avatars1.githubusercontent.com/u/11620174?s=400&amp;v=4" /><meta name="twitter:site" content="@github" /><meta name="twitter:card" content="summary" /><meta name="twitter:title" content="spdx/license-list-data" /><meta name="twitter:description" content="Various data formats for the SPDX License List including RDFa, HTML, Text, and JSON - spdx/license-list-data" />
+ <meta property="og:image" content="https://avatars1.githubusercontent.com/u/11620174?s=400&amp;v=4" /><meta property="og:site_name" content="GitHub" /><meta property="og:type" content="object" /><meta property="og:title" content="spdx/license-list-data" /><meta property="og:url" content="https://github.com/spdx/license-list-data" /><meta property="og:description" content="Various data formats for the SPDX License List including RDFa, HTML, Text, and JSON - spdx/license-list-data" />
+
+
+
+
+
+ <link rel="assets" href="https://github.githubassets.com/">
+
+
+ <meta name="request-id" content="B4B8:10D0D:4AB4450:620BA1E:5FC149E3" data-pjax-transient="true"/><meta name="html-safe-nonce" content="e521d3bf3cc20c0fb986800d2ee23e258811bcf262b2af3936077fad1334571c" data-pjax-transient="true"/><meta name="visitor-payload" content="eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCNEI4OjEwRDBEOjRBQjQ0NTA6NjIwQkExRTo1RkMxNDlFMyIsInZpc2l0b3JfaWQiOiI4OTg4MTY4MzcyMjIzMDM1ODc1IiwicmVnaW9uX2VkZ2UiOiJmcmEiLCJyZWdpb25fcmVuZGVyIjoiZnJhIn0=" data-pjax-transient="true"/><meta name="visitor-hmac" content="174b297df77d9ca3ac3ef068cc0ade8b1de82f20ba2146e666e50e640bffd73e" data-pjax-transient="true"/><meta name="cookie-consent-required" content="true" data-pjax-transient="true"/>
+
+ <meta name="hovercard-subject-tag" content="repository:56354042" data-pjax-transient>
+
+
+ <meta name="github-keyboard-shortcuts" content="repository,source-code" data-pjax-transient="true" />
+
+
+
+ <meta name="selected-link" value="repo_source" data-pjax-transient>
+
+ <meta name="google-site-verification" content="c1kuD-K2HIVF635lypcsWPoD4kilo5-jA_wBFyT4uMY">
+ <meta name="google-site-verification" content="KT5gs8h0wvaagLKAVWq8bbeNwnZZK1r1XQysX3xurLU">
+ <meta name="google-site-verification" content="ZzhVyEFwb7w3e0-uOTltm8Jsck2F5StVihD0exw2fsA">
+ <meta name="google-site-verification" content="GXs5KoUUkNCoaAZn7wPN-t01Pywp9M3sEjnt_3_ZWPc">
+
+ <meta name="octolytics-host" content="collector.githubapp.com" /><meta name="octolytics-app-id" content="github" /><meta name="octolytics-event-url" content="https://collector.githubapp.com/github-external/browser_event" />
+
+ <meta name="analytics-location" content="/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show" data-pjax-transient="true" />
+
+
+
+
+
+ <meta name="optimizely-datafile" content="{&quot;version&quot;: &quot;4&quot;, &quot;rollouts&quot;: [], &quot;typedAudiences&quot;: [], &quot;anonymizeIP&quot;: true, &quot;projectId&quot;: &quot;16737760170&quot;, &quot;variables&quot;: [], &quot;featureFlags&quot;: [], &quot;experiments&quot;: [{&quot;status&quot;: &quot;Running&quot;, &quot;audienceIds&quot;: [], &quot;variations&quot;: [{&quot;variables&quot;: [], &quot;id&quot;: &quot;18630402174&quot;, &quot;key&quot;: &quot;launchpad&quot;}, {&quot;variables&quot;: [], &quot;id&quot;: &quot;18866331456&quot;, &quot;key&quot;: &quot;control&quot;}], &quot;id&quot;: &quot;18651193356&quot;, &quot;key&quot;: &quot;_features_redesign_rollout&quot;, &quot;layerId&quot;: &quot;18645992876&quot;, &quot;trafficAllocation&quot;: [{&quot;entityId&quot;: &quot;18630402174&quot;, &quot;endOfRange&quot;: 500}, {&quot;entityId&quot;: &quot;18866331456&quot;, &quot;endOfRange&quot;: 1000}, {&quot;entityId&quot;: &quot;18630402174&quot;, &quot;endOfRange&quot;: 5000}, {&quot;entityId&quot;: &quot;18630402174&quot;, &quot;endOfRange&quot;: 5500}, {&quot;entityId&quot;: &quot;18866331456&quot;, &quot;endOfRange&quot;: 10000}], &quot;forcedVariations&quot;: {&quot;143327983.1601483920&quot;: &quot;launchpad&quot;, &quot;1955030087.1562868941&quot;: &quot;launchpad&quot;, &quot;1983887325.1550021416&quot;: &quot;launchpad&quot;, &quot;1947530619.1600461583&quot;: &quot;launchpad&quot;}}, {&quot;status&quot;: &quot;Running&quot;, &quot;audienceIds&quot;: [], &quot;variations&quot;: [{&quot;variables&quot;: [], &quot;id&quot;: &quot;19136700362&quot;, &quot;key&quot;: &quot;show_plans&quot;}, {&quot;variables&quot;: [], &quot;id&quot;: &quot;19157700511&quot;, &quot;key&quot;: &quot;control&quot;}], &quot;id&quot;: &quot;19062314978&quot;, &quot;key&quot;: &quot;account_billing_plans&quot;, &quot;layerId&quot;: &quot;19068014945&quot;, &quot;trafficAllocation&quot;: [{&quot;entityId&quot;: &quot;19136700362&quot;, &quot;endOfRange&quot;: 5000}, {&quot;entityId&quot;: &quot;19157700511&quot;, &quot;endOfRange&quot;: 10000}], &quot;forcedVariations&quot;: {&quot;1238720267648ea2c88a74b410aa3c5c&quot;: &quot;show_plans&quot;, &quot;c4abf59d1620c671458b2a74df2a2410&quot;: &quot;control&quot;}}], &quot;audiences&quot;: [{&quot;conditions&quot;: &quot;[\&quot;or\&quot;, {\&quot;match\&quot;: \&quot;exact\&quot;, \&quot;name\&quot;: \&quot;$opt_dummy_attribute\&quot;, \&quot;type\&quot;: \&quot;custom_attribute\&quot;, \&quot;value\&quot;: \&quot;$opt_dummy_value\&quot;}]&quot;, &quot;id&quot;: &quot;$opt_dummy_audience&quot;, &quot;name&quot;: &quot;Optimizely-Generated Audience for Backwards Compatibility&quot;}], &quot;groups&quot;: [], &quot;attributes&quot;: [{&quot;id&quot;: &quot;16822470375&quot;, &quot;key&quot;: &quot;user_id&quot;}, {&quot;id&quot;: &quot;17143601254&quot;, &quot;key&quot;: &quot;spammy&quot;}, {&quot;id&quot;: &quot;18175660309&quot;, &quot;key&quot;: &quot;organization_plan&quot;}, {&quot;id&quot;: &quot;18813001570&quot;, &quot;key&quot;: &quot;is_logged_in&quot;}, {&quot;id&quot;: &quot;19073851829&quot;, &quot;key&quot;: &quot;geo&quot;}], &quot;botFiltering&quot;: false, &quot;accountId&quot;: &quot;16737760170&quot;, &quot;events&quot;: [{&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;17911811441&quot;, &quot;key&quot;: &quot;hydro_click.dashboard.teacher_toolbox_cta&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18124116703&quot;, &quot;key&quot;: &quot;submit.organizations.complete_sign_up&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18145892387&quot;, &quot;key&quot;: &quot;no_metric.tracked_outside_of_optimizely&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18178755568&quot;, &quot;key&quot;: &quot;click.org_onboarding_checklist.add_repo&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18180553241&quot;, &quot;key&quot;: &quot;submit.repository_imports.create&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18186103728&quot;, &quot;key&quot;: &quot;click.help.learn_more_about_repository_creation&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18188530140&quot;, &quot;key&quot;: &quot;test_event.do_not_use_in_production&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18191963644&quot;, &quot;key&quot;: &quot;click.empty_org_repo_cta.transfer_repository&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18195612788&quot;, &quot;key&quot;: &quot;click.empty_org_repo_cta.import_repository&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18210945499&quot;, &quot;key&quot;: &quot;click.org_onboarding_checklist.invite_members&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18211063248&quot;, &quot;key&quot;: &quot;click.empty_org_repo_cta.create_repository&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18215721889&quot;, &quot;key&quot;: &quot;click.org_onboarding_checklist.update_profile&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18224360785&quot;, &quot;key&quot;: &quot;click.org_onboarding_checklist.dismiss&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18234832286&quot;, &quot;key&quot;: &quot;submit.organization_activation.complete&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18252392383&quot;, &quot;key&quot;: &quot;submit.org_repository.create&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18257551537&quot;, &quot;key&quot;: &quot;submit.org_member_invitation.create&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18259522260&quot;, &quot;key&quot;: &quot;submit.organization_profile.update&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18564603625&quot;, &quot;key&quot;: &quot;view.classroom_select_organization&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18568612016&quot;, &quot;key&quot;: &quot;click.classroom_sign_in_click&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18572592540&quot;, &quot;key&quot;: &quot;view.classroom_name&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18574203855&quot;, &quot;key&quot;: &quot;click.classroom_create_organization&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18582053415&quot;, &quot;key&quot;: &quot;click.classroom_select_organization&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18589463420&quot;, &quot;key&quot;: &quot;click.classroom_create_classroom&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18591323364&quot;, &quot;key&quot;: &quot;click.classroom_create_first_classroom&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18591652321&quot;, &quot;key&quot;: &quot;click.classroom_grant_access&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18607131425&quot;, &quot;key&quot;: &quot;view.classroom_creation&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;18831680583&quot;, &quot;key&quot;: &quot;upgrade_account_plan&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;19064064515&quot;, &quot;key&quot;: &quot;click.signup&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;19075373687&quot;, &quot;key&quot;: &quot;click.view_account_billing_page&quot;}, {&quot;experimentIds&quot;: [], &quot;id&quot;: &quot;19077355841&quot;, &quot;key&quot;: &quot;click.dismiss_signup_prompt&quot;}, {&quot;experimentIds&quot;: [&quot;19062314978&quot;], &quot;id&quot;: &quot;19079713938&quot;, &quot;key&quot;: &quot;click.contact_sales&quot;}, {&quot;experimentIds&quot;: [&quot;19062314978&quot;], &quot;id&quot;: &quot;19120963070&quot;, &quot;key&quot;: &quot;click.compare_account_plans&quot;}, {&quot;experimentIds&quot;: [&quot;19062314978&quot;], &quot;id&quot;: &quot;19151690317&quot;, &quot;key&quot;: &quot;click.upgrade_account_cta&quot;}], &quot;revision&quot;: &quot;338&quot;}" />
+ <!-- To prevent page flashing, the optimizely JS needs to be loaded in the
+ <head> tag before the DOM renders -->
+ <script crossorigin="anonymous" defer="defer" integrity="sha512-VJrqSK702Mzl9EQxm2OvFxKaumGptgVdeJS2rsaLvVlOdR4HEu3ZFjtV83kMKdYRelUnxxaAFw0wthkpdEUafw==" type="application/javascript" src="https://github.githubassets.com/assets/optimizely-549aea48.js"></script>
+
+
+
+
+
+ <meta name="hostname" content="github.com">
+ <meta name="user-login" content="">
+
+
+ <meta name="expected-hostname" content="github.com">
+
+
+ <meta name="enabled-features" content="MARKETPLACE_PENDING_INSTALLATIONS">
+
+ <meta http-equiv="x-pjax-version" content="2e8f71f42810547327b61d588a3a343a43cfff382213dc6ed94d44854f0d23c0">
+
+
+ <link href="https://github.com/spdx/license-list-data/commits/master.atom" rel="alternate" title="Recent Commits to license-list-data:master" type="application/atom+xml">
+
+ <meta name="go-import" content="github.com/spdx/license-list-data git https://github.com/spdx/license-list-data.git">
+
+ <meta name="octolytics-dimension-user_id" content="11620174" /><meta name="octolytics-dimension-user_login" content="spdx" /><meta name="octolytics-dimension-repository_id" content="56354042" /><meta name="octolytics-dimension-repository_nwo" content="spdx/license-list-data" /><meta name="octolytics-dimension-repository_public" content="true" /><meta name="octolytics-dimension-repository_is_fork" content="false" /><meta name="octolytics-dimension-repository_network_root_id" content="56354042" /><meta name="octolytics-dimension-repository_network_root_nwo" content="spdx/license-list-data" /><meta name="octolytics-dimension-repository_explore_github_marketplace_ci_cta_shown" content="false" />
+
+
+ <link rel="canonical" href="https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt" data-pjax-transient>
+
+
+ <meta name="browser-stats-url" content="https://api.github.com/_private/browser/stats">
+
+ <meta name="browser-errors-url" content="https://api.github.com/_private/browser/errors">
+
+ <meta name="browser-optimizely-client-errors-url" content="https://api.github.com/_private/browser/optimizely_client/errors">
+
+ <link rel="mask-icon" href="https://github.githubassets.com/pinned-octocat.svg" color="#000000">
+ <link rel="alternate icon" class="js-site-favicon" type="image/png" href="https://github.githubassets.com/favicons/favicon.png">
+ <link rel="icon" class="js-site-favicon" type="image/svg+xml" href="https://github.githubassets.com/favicons/favicon.svg">
+
+<meta name="theme-color" content="#1e2327">
+
+
+ <link rel="manifest" href="/manifest.json" crossOrigin="use-credentials">
+
+ </head>
+
+ <body class="logged-out env-production page-responsive page-blob">
+
+
+ <div class="position-relative js-header-wrapper ">
+ <a href="#start-of-content" class="px-2 py-4 bg-blue text-white show-on-focus js-skip-to-content">Skip to content</a>
+ <span class="progress-pjax-loader width-full js-pjax-loader-bar Progress position-fixed">
+ <span style="background-color: #79b8ff;width: 0%;" class="Progress-item progress-pjax-loader-bar "></span>
+</span>
+
+
+
+ <header class="Header-old header-logged-out js-details-container Details position-relative f4 py-2" role="banner">
+ <div class="container-xl d-lg-flex flex-items-center p-responsive">
+ <div class="d-flex flex-justify-between flex-items-center">
+ <a class="mr-4" href="https://github.com/" aria-label="Homepage" data-ga-click="(Logged out) Header, go to homepage, icon:logo-wordmark">
+ <svg height="32" class="octicon octicon-mark-github text-white" viewBox="0 0 16 16" version="1.1" width="32" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
+ </a>
+
+ <div class="d-lg-none css-truncate css-truncate-target width-fit p-2">
+
+
+ </div>
+
+ <div class="d-flex flex-items-center">
+ <a href="/join?ref_cta=Sign+up&amp;ref_loc=header+logged+out&amp;ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fblob%2Fshow&amp;source=header-repo"
+ class="d-inline-block d-lg-none f5 text-white no-underline border border-gray-dark rounded-2 px-2 py-1 mr-3 mr-sm-5"
+ data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;site header&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;SIGN_UP&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="34920eb210d5be055acca5c12c6b352bf9b2de6885362e957625e85dd892c34f"
+ data-ga-click="Sign up, click to sign up for account, ref_page:/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show;ref_cta:Sign up;ref_loc:header logged out">
+ Sign&nbsp;up
+ </a>
+
+ <button class="btn-link d-lg-none mt-1 js-details-target" type="button" aria-label="Toggle navigation" aria-expanded="false">
+ <svg height="24" class="octicon octicon-three-bars text-white" viewBox="0 0 16 16" version="1.1" width="24" aria-hidden="true"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>
+ </button>
+ </div>
+ </div>
+
+ <div class="HeaderMenu HeaderMenu--logged-out position-fixed top-0 right-0 bottom-0 height-fit position-lg-relative d-lg-flex flex-justify-between flex-items-center flex-auto">
+ <div class="d-flex d-lg-none flex-justify-end border-bottom bg-gray-light p-3">
+ <button class="btn-link js-details-target" type="button" aria-label="Toggle navigation" aria-expanded="false">
+ <svg height="24" class="octicon octicon-x text-gray" viewBox="0 0 24 24" version="1.1" width="24" aria-hidden="true"><path fill-rule="evenodd" d="M5.72 5.72a.75.75 0 011.06 0L12 10.94l5.22-5.22a.75.75 0 111.06 1.06L13.06 12l5.22 5.22a.75.75 0 11-1.06 1.06L12 13.06l-5.22 5.22a.75.75 0 01-1.06-1.06L10.94 12 5.72 6.78a.75.75 0 010-1.06z"></path></svg>
+ </button>
+ </div>
+
+ <nav class="mt-0 px-3 px-lg-0 mb-5 mb-lg-0" aria-label="Global">
+ <ul class="d-lg-flex list-style-none">
+ <li class="d-block d-lg-flex flex-lg-nowrap flex-lg-items-center border-bottom border-lg-bottom-0 mr-0 mr-lg-3 edge-item-fix position-relative flex-wrap flex-justify-between d-flex flex-items-center ">
+ <details class="HeaderMenu-details details-overlay details-reset width-full">
+ <summary class="HeaderMenu-summary HeaderMenu-link px-0 py-3 border-0 no-wrap d-block d-lg-inline-block">
+ Why GitHub?
+ <svg x="0px" y="0px" viewBox="0 0 14 8" xml:space="preserve" fill="none" class="icon-chevon-down-mktg position-absolute position-lg-relative">
+ <path d="M1,1l6.2,6L13,1"></path>
+ </svg>
+ </summary>
+ <div class="dropdown-menu flex-auto rounded-1 bg-white px-0 mt-0 pb-4 p-lg-4 position-relative position-lg-absolute left-0 left-lg-n4">
+ <a href="/features" class="py-2 lh-condensed-ultra d-block link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Features">Features <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a>
+ <ul class="list-style-none f5 pb-3">
+ <li class="edge-item-fix"><a href="/features/code-review/" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Code review">Code review</a></li>
+ <li class="edge-item-fix"><a href="/features/project-management/" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Project management">Project management</a></li>
+ <li class="edge-item-fix"><a href="/features/integrations" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Integrations">Integrations</a></li>
+ <li class="edge-item-fix"><a href="/features/actions" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Actions">Actions</a></li>
+ <li class="edge-item-fix"><a href="/features/packages" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to GitHub Packages">Packages</a></li>
+ <li class="edge-item-fix"><a href="/features/security" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Security">Security</a></li>
+ <li class="edge-item-fix"><a href="/features#team-management" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Team management">Team management</a></li>
+ <li class="edge-item-fix"><a href="/features#hosting" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Code hosting">Hosting</a></li>
+ <li class="edge-item-fix hide-xl"><a href="/mobile" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Mobile">Mobile</a></li>
+ </ul>
+
+ <ul class="list-style-none mb-0 border-lg-top pt-lg-3">
+ <li class="edge-item-fix"><a href="/customer-stories" class="py-2 lh-condensed-ultra d-block no-underline link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Customer stories">Customer stories <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a></li>
+ <li class="edge-item-fix"><a href="/security" class="py-2 lh-condensed-ultra d-block no-underline link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Security">Security <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a></li>
+ </ul>
+ </div>
+ </details>
+ </li>
+ <li class="border-bottom border-lg-bottom-0 mr-0 mr-lg-3">
+ <a href="/team" class="HeaderMenu-link no-underline py-3 d-block d-lg-inline-block" data-ga-click="(Logged out) Header, go to Team">Team</a>
+ </li>
+ <li class="border-bottom border-lg-bottom-0 mr-0 mr-lg-3">
+ <a href="/enterprise" class="HeaderMenu-link no-underline py-3 d-block d-lg-inline-block" data-ga-click="(Logged out) Header, go to Enterprise">Enterprise</a>
+ </li>
+
+ <li class="d-block d-lg-flex flex-lg-nowrap flex-lg-items-center border-bottom border-lg-bottom-0 mr-0 mr-lg-3 edge-item-fix position-relative flex-wrap flex-justify-between d-flex flex-items-center ">
+ <details class="HeaderMenu-details details-overlay details-reset width-full">
+ <summary class="HeaderMenu-summary HeaderMenu-link px-0 py-3 border-0 no-wrap d-block d-lg-inline-block">
+ Explore
+ <svg x="0px" y="0px" viewBox="0 0 14 8" xml:space="preserve" fill="none" class="icon-chevon-down-mktg position-absolute position-lg-relative">
+ <path d="M1,1l6.2,6L13,1"></path>
+ </svg>
+ </summary>
+
+ <div class="dropdown-menu flex-auto rounded-1 bg-white px-0 pt-2 pb-0 mt-0 pb-4 p-lg-4 position-relative position-lg-absolute left-0 left-lg-n4">
+ <ul class="list-style-none mb-3">
+ <li class="edge-item-fix"><a href="/explore" class="py-2 lh-condensed-ultra d-block link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Explore">Explore GitHub <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a></li>
+ </ul>
+
+ <h4 class="text-gray-light text-normal text-mono f5 mb-2 border-lg-top pt-lg-3">Learn &amp; contribute</h4>
+ <ul class="list-style-none mb-3">
+ <li class="edge-item-fix"><a href="/topics" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Topics">Topics</a></li>
+ <li class="edge-item-fix"><a href="/collections" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Collections">Collections</a></li>
+ <li class="edge-item-fix"><a href="/trending" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Trending">Trending</a></li>
+ <li class="edge-item-fix"><a href="https://lab.github.com/" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Learning lab">Learning Lab</a></li>
+ <li class="edge-item-fix"><a href="https://opensource.guide" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Open source guides">Open source guides</a></li>
+ </ul>
+
+ <h4 class="text-gray-light text-normal text-mono f5 mb-2 border-lg-top pt-lg-3">Connect with others</h4>
+ <ul class="list-style-none mb-0">
+ <li class="edge-item-fix"><a href="https://github.com/events" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Events">Events</a></li>
+ <li class="edge-item-fix"><a href="https://github.community" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Community forum">Community forum</a></li>
+ <li class="edge-item-fix"><a href="https://education.github.com" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to GitHub Education">GitHub Education</a></li>
+ <li class="edge-item-fix"><a href="https://stars.github.com" class="py-2 pb-0 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to GitHub Stars Program">GitHub Stars program</a></li>
+ </ul>
+ </div>
+ </details>
+ </li>
+
+ <li class="border-bottom border-lg-bottom-0 mr-0 mr-lg-3">
+ <a href="/marketplace" class="HeaderMenu-link no-underline py-3 d-block d-lg-inline-block" data-ga-click="(Logged out) Header, go to Marketplace">Marketplace</a>
+ </li>
+
+ <li class="d-block d-lg-flex flex-lg-nowrap flex-lg-items-center border-bottom border-lg-bottom-0 mr-0 mr-lg-3 edge-item-fix position-relative flex-wrap flex-justify-between d-flex flex-items-center ">
+ <details class="HeaderMenu-details details-overlay details-reset width-full">
+ <summary class="HeaderMenu-summary HeaderMenu-link px-0 py-3 border-0 no-wrap d-block d-lg-inline-block">
+ Pricing
+ <svg x="0px" y="0px" viewBox="0 0 14 8" xml:space="preserve" fill="none" class="icon-chevon-down-mktg position-absolute position-lg-relative">
+ <path d="M1,1l6.2,6L13,1"></path>
+ </svg>
+ </summary>
+
+ <div class="dropdown-menu flex-auto rounded-1 bg-white px-0 pt-2 pb-4 mt-0 p-lg-4 position-relative position-lg-absolute left-0 left-lg-n4">
+ <a href="/pricing" class="pb-2 lh-condensed-ultra d-block link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Pricing">Plans <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a>
+
+ <ul class="list-style-none mb-3">
+ <li class="edge-item-fix"><a href="/pricing#feature-comparison" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Compare plans">Compare plans</a></li>
+ <li class="edge-item-fix"><a href="https://enterprise.github.com/contact" class="py-2 lh-condensed-ultra d-block link-gray no-underline f5" data-ga-click="(Logged out) Header, go to Contact Sales">Contact Sales</a></li>
+ </ul>
+
+ <ul class="list-style-none mb-0 border-lg-top pt-lg-3">
+ <li class="edge-item-fix"><a href="/nonprofit" class="py-2 lh-condensed-ultra d-block no-underline link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Nonprofits">Nonprofit <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a></li>
+ <li class="edge-item-fix"><a href="https://education.github.com" class="py-2 pb-0 lh-condensed-ultra d-block no-underline link-gray-dark no-underline h5 Bump-link--hover" data-ga-click="(Logged out) Header, go to Education">Education <span class="Bump-link-symbol float-right text-normal text-gray-light">&rarr;</span></a></li>
+ </ul>
+ </div>
+ </details>
+ </li>
+ </ul>
+ </nav>
+
+ <div class="d-lg-flex flex-items-center px-3 px-lg-0 text-center text-lg-left">
+ <div class="d-lg-flex mb-3 mb-lg-0">
+ <div class="header-search flex-auto js-site-search position-relative flex-self-stretch flex-md-self-auto mb-3 mb-md-0 mr-0 mr-md-3 scoped-search site-scoped-search js-jump-to"
+ role="combobox"
+ aria-owns="jump-to-results"
+ aria-label="Search or jump to"
+ aria-haspopup="listbox"
+ aria-expanded="false"
+>
+ <div class="position-relative">
+ <!-- '"` --><!-- </textarea></xmp> --></option></form><form class="js-site-search-form" role="search" aria-label="Site" data-scope-type="Repository" data-scope-id="56354042" data-scoped-search-url="/spdx/license-list-data/search" data-owner-scoped-search-url="/orgs/spdx/search" data-unscoped-search-url="/search" action="/spdx/license-list-data/search" accept-charset="UTF-8" method="get">
+ <label class="form-control input-sm header-search-wrapper p-0 js-chromeless-input-container header-search-wrapper-jump-to position-relative d-flex flex-justify-between flex-items-center">
+ <input type="text"
+ class="form-control input-sm header-search-input jump-to-field js-jump-to-field js-site-search-focus js-site-search-field is-clearable"
+ data-hotkey="s,/"
+ name="q"
+ value=""
+ placeholder="Search"
+ data-unscoped-placeholder="Search GitHub"
+ data-scoped-placeholder="Search"
+ autocapitalize="off"
+ aria-autocomplete="list"
+ aria-controls="jump-to-results"
+ aria-label="Search"
+ data-jump-to-suggestions-path="/_graphql/GetSuggestedNavigationDestinations"
+ spellcheck="false"
+ autocomplete="off"
+ >
+ <input type="hidden" data-csrf="true" class="js-data-jump-to-suggestions-path-csrf" value="aGkm3j0RLXNLh7u43Y7k0rSwR4Zju88KYkUFlFrAJT4LjEPo5EdC63cF7BW3bZj6q8zGi6MVsfmqLU9cJj1kmg==" />
+ <input type="hidden" class="js-site-search-type-field" name="type" >
+ <img src="https://github.githubassets.com/images/search-key-slash.svg" alt="" class="mr-2 header-search-key-slash">
+
+ <div class="Box position-absolute overflow-hidden d-none jump-to-suggestions js-jump-to-suggestions-container">
+
+<ul class="d-none js-jump-to-suggestions-template-container">
+
+
+<li class="d-flex flex-justify-start flex-items-center p-0 f5 navigation-item js-navigation-item js-jump-to-suggestion" role="option">
+ <a tabindex="-1" class="no-underline d-flex flex-auto flex-items-center jump-to-suggestions-path js-jump-to-suggestion-path js-navigation-open p-2" href="" data-item-type="suggestion">
+ <div class="jump-to-octicon js-jump-to-octicon flex-shrink-0 mr-2 text-center d-none">
+ <svg height="16" width="16" class="octicon octicon-repo flex-shrink-0 js-jump-to-octicon-repo d-none" title="Repository" aria-label="Repository" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-project flex-shrink-0 js-jump-to-octicon-project d-none" title="Project" aria-label="Project" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-search flex-shrink-0 js-jump-to-octicon-search d-none" title="Search" aria-label="Search" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>
+ </div>
+
+ <img class="avatar mr-2 flex-shrink-0 js-jump-to-suggestion-avatar d-none" alt="" aria-label="Team" src="" width="28" height="28">
+
+ <div class="jump-to-suggestion-name js-jump-to-suggestion-name flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target">
+ </div>
+
+ <div class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none js-jump-to-badge-search">
+ <span class="js-jump-to-badge-search-text-default d-none" aria-label="in this repository">
+ In this repository
+ </span>
+ <span class="js-jump-to-badge-search-text-global d-none" aria-label="in all of GitHub">
+ All GitHub
+ </span>
+ <span aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+
+ <div aria-hidden="true" class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none d-on-nav-focus js-jump-to-badge-jump">
+ Jump to
+ <span class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+ </a>
+</li>
+
+</ul>
+
+<ul class="d-none js-jump-to-no-results-template-container">
+ <li class="d-flex flex-justify-center flex-items-center f5 d-none js-jump-to-suggestion p-2">
+ <span class="text-gray">No suggested jump to results</span>
+ </li>
+</ul>
+
+<ul id="jump-to-results" role="listbox" class="p-0 m-0 js-navigation-container jump-to-suggestions-results-container js-jump-to-suggestions-results-container">
+
+
+<li class="d-flex flex-justify-start flex-items-center p-0 f5 navigation-item js-navigation-item js-jump-to-scoped-search d-none" role="option">
+ <a tabindex="-1" class="no-underline d-flex flex-auto flex-items-center jump-to-suggestions-path js-jump-to-suggestion-path js-navigation-open p-2" href="" data-item-type="scoped_search">
+ <div class="jump-to-octicon js-jump-to-octicon flex-shrink-0 mr-2 text-center d-none">
+ <svg height="16" width="16" class="octicon octicon-repo flex-shrink-0 js-jump-to-octicon-repo d-none" title="Repository" aria-label="Repository" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-project flex-shrink-0 js-jump-to-octicon-project d-none" title="Project" aria-label="Project" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-search flex-shrink-0 js-jump-to-octicon-search d-none" title="Search" aria-label="Search" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>
+ </div>
+
+ <img class="avatar mr-2 flex-shrink-0 js-jump-to-suggestion-avatar d-none" alt="" aria-label="Team" src="" width="28" height="28">
+
+ <div class="jump-to-suggestion-name js-jump-to-suggestion-name flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target">
+ </div>
+
+ <div class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none js-jump-to-badge-search">
+ <span class="js-jump-to-badge-search-text-default d-none" aria-label="in this repository">
+ In this repository
+ </span>
+ <span class="js-jump-to-badge-search-text-global d-none" aria-label="in all of GitHub">
+ All GitHub
+ </span>
+ <span aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+
+ <div aria-hidden="true" class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none d-on-nav-focus js-jump-to-badge-jump">
+ Jump to
+ <span class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+ </a>
+</li>
+
+
+
+<li class="d-flex flex-justify-start flex-items-center p-0 f5 navigation-item js-navigation-item js-jump-to-global-search d-none" role="option">
+ <a tabindex="-1" class="no-underline d-flex flex-auto flex-items-center jump-to-suggestions-path js-jump-to-suggestion-path js-navigation-open p-2" href="" data-item-type="global_search">
+ <div class="jump-to-octicon js-jump-to-octicon flex-shrink-0 mr-2 text-center d-none">
+ <svg height="16" width="16" class="octicon octicon-repo flex-shrink-0 js-jump-to-octicon-repo d-none" title="Repository" aria-label="Repository" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-project flex-shrink-0 js-jump-to-octicon-project d-none" title="Project" aria-label="Project" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
+ <svg height="16" width="16" class="octicon octicon-search flex-shrink-0 js-jump-to-octicon-search d-none" title="Search" aria-label="Search" viewBox="0 0 16 16" version="1.1" role="img"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>
+ </div>
+
+ <img class="avatar mr-2 flex-shrink-0 js-jump-to-suggestion-avatar d-none" alt="" aria-label="Team" src="" width="28" height="28">
+
+ <div class="jump-to-suggestion-name js-jump-to-suggestion-name flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target">
+ </div>
+
+ <div class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none js-jump-to-badge-search">
+ <span class="js-jump-to-badge-search-text-default d-none" aria-label="in this repository">
+ In this repository
+ </span>
+ <span class="js-jump-to-badge-search-text-global d-none" aria-label="in all of GitHub">
+ All GitHub
+ </span>
+ <span aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+
+ <div aria-hidden="true" class="border rounded-1 flex-shrink-0 bg-gray px-1 text-gray-light ml-1 f6 d-none d-on-nav-focus js-jump-to-badge-jump">
+ Jump to
+ <span class="d-inline-block ml-1 v-align-middle">↵</span>
+ </div>
+ </a>
+</li>
+
+
+</ul>
+
+ </div>
+ </label>
+</form> </div>
+</div>
+
+ </div>
+
+ <a href="/login?return_to=%2Fspdx%2Flicense-list-data%2Fblob%2Fmaster%2Ftext%2FCC0-1.0.txt"
+ class="HeaderMenu-link no-underline mr-3"
+ data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;site header menu&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;SIGN_UP&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="9657c9a3df5a4fafa5ab8efb43a77950bda7b6344259de18debac0b6a827c114"
+ data-ga-click="(Logged out) Header, clicked Sign in, text:sign-in">
+ Sign&nbsp;in
+ </a>
+ <a href="/join?ref_cta=Sign+up&amp;ref_loc=header+logged+out&amp;ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fblob%2Fshow&amp;source=header-repo&amp;source_repo=spdx%2Flicense-list-data"
+ class="HeaderMenu-link d-inline-block no-underline border border-gray-dark rounded-1 px-2 py-1"
+ data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;site header menu&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;SIGN_UP&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="9657c9a3df5a4fafa5ab8efb43a77950bda7b6344259de18debac0b6a827c114"
+ data-ga-click="Sign up, click to sign up for account, ref_page:/&lt;user-name&gt;/&lt;repo-name&gt;/blob/show;ref_cta:Sign up;ref_loc:header logged out">
+ Sign&nbsp;up
+ </a>
+ </div>
+ </div>
+ </div>
+</header>
+
+ </div>
+
+ <div id="start-of-content" class="show-on-focus"></div>
+
+
+
+
+
+ <div data-pjax-replace id="js-flash-container">
+
+
+ <template class="js-flash-template">
+ <div class="flash flash-full {{ className }}">
+ <div class=" px-2" >
+ <button class="flash-close js-flash-close" type="button" aria-label="Dismiss this message">
+ <svg class="octicon octicon-x" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>
+ </button>
+
+ <div>{{ message }}</div>
+
+ </div>
+</div>
+ </template>
+</div>
+
+
+
+
+ <include-fragment class="js-notification-shelf-include-fragment" data-base-src="https://github.com/notifications/beta/shelf"></include-fragment>
+
+
+
+
+ <div
+ class="application-main "
+ data-commit-hovercards-enabled
+ data-discussion-hovercards-enabled
+ data-issue-and-pr-hovercards-enabled
+ >
+ <div itemscope itemtype="http://schema.org/SoftwareSourceCode" class="">
+ <main >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <div class="bg-gray-light pt-3 hide-full-screen mb-5">
+
+ <div class="d-flex mb-3 px-3 px-md-4 px-lg-5">
+
+ <div class="flex-auto min-width-0 width-fit mr-3">
+ <h1 class=" d-flex flex-wrap flex-items-center break-word f3 text-normal">
+ <svg class="octicon octicon-repo text-gray mr-2" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
+ <span class="author flex-self-stretch" itemprop="author">
+ <a class="url fn" rel="author" data-hovercard-type="organization" data-hovercard-url="/orgs/spdx/hovercard" href="/spdx">spdx</a>
+ </span>
+ <span class="mx-1 flex-self-stretch color-text-secondary">/</span>
+ <strong itemprop="name" class="mr-2 flex-self-stretch">
+ <a data-pjax="#js-repo-pjax-container" class="" href="/spdx/license-list-data">license-list-data</a>
+ </strong>
+
+</h1>
+
+
+ </div>
+
+ <ul class="pagehead-actions flex-shrink-0 d-none d-md-inline" style="padding: 2px 0;">
+
+ <li>
+ <a class="tooltipped tooltipped-s btn btn-sm btn-with-count" aria-label="You must be signed in to watch a repository" rel="nofollow" data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;notification subscription menu watch&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;LOG_IN&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="301c03190401c2aafd0e546c78435f41adfbc7223428f3cc909b942664cf80a9" href="/login?return_to=%2Fspdx%2Flicense-list-data">
+ <svg class="octicon octicon-eye" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
+ Watch
+</a> <a class="social-count" href="/spdx/license-list-data/watchers"
+ aria-label="21 users are watching this repository">
+ 21
+ </a>
+
+ </li>
+
+ <li>
+ <a class="btn btn-sm btn-with-count tooltipped tooltipped-s" aria-label="You must be signed in to star a repository" rel="nofollow" data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;star button&quot;,&quot;repository_id&quot;:56354042,&quot;auth_type&quot;:&quot;LOG_IN&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="c8c6d607b0bd5a22b65d46847e73aea32a048a36d2c7569c047188f7c7c2b094" href="/login?return_to=%2Fspdx%2Flicense-list-data">
+ <svg class="octicon octicon-star v-align-text-bottom" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
+ Star
+</a>
+ <a class="social-count js-social-count" href="/spdx/license-list-data/stargazers"
+ aria-label="159 users starred this repository">
+ 159
+ </a>
+
+ </li>
+
+ <li>
+ <a class="btn btn-sm btn-with-count tooltipped tooltipped-s" aria-label="You must be signed in to fork a repository" rel="nofollow" data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;repo details fork button&quot;,&quot;repository_id&quot;:56354042,&quot;auth_type&quot;:&quot;LOG_IN&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="e45807236d963f0332ee5b00478713016b938efea197da6c7d3b1ecccafc1241" href="/login?return_to=%2Fspdx%2Flicense-list-data">
+ <svg class="octicon octicon-repo-forked" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
+ Fork
+</a>
+ <a href="/spdx/license-list-data/network/members" class="social-count"
+ aria-label="62 users forked this repository">
+ 62
+ </a>
+ </li>
+</ul>
+
+ </div>
+
+<nav aria-label="Repository" data-pjax="#js-repo-pjax-container" class="js-repo-nav js-sidenav-container-pjax js-responsive-underlinenav overflow-hidden UnderlineNav px-3 px-md-4 px-lg-5 bg-gray-light">
+ <ul class="UnderlineNav-body list-style-none ">
+ <li class="d-flex">
+ <a class="js-selected-navigation-item selected UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="code-tab" data-hotkey="g c" data-ga-click="Repository, Navigation click, Code tab" aria-current="page" data-selected-links="repo_source repo_downloads repo_commits repo_releases repo_tags repo_branches repo_packages repo_deployments /spdx/license-list-data" href="/spdx/license-list-data">
+ <svg class="octicon octicon-code UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>
+ <span data-content="Code">Code</span>
+ <span title="Not available" class="Counter "></span>
+</a> </li>
+ <li class="d-flex">
+ <a class="js-selected-navigation-item UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="pull-requests-tab" data-hotkey="g p" data-ga-click="Repository, Navigation click, Pull requests tab" data-selected-links="repo_pulls checks /spdx/license-list-data/pulls" href="/spdx/license-list-data/pulls">
+ <svg class="octicon octicon-git-pull-request UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
+ <span data-content="Pull requests">Pull requests</span>
+ <span title="0" hidden="hidden" class="Counter ">0</span>
+</a> </li>
+ <li class="d-flex">
+ <a class="js-selected-navigation-item UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="actions-tab" data-hotkey="g a" data-ga-click="Repository, Navigation click, Actions tab" data-selected-links="repo_actions /spdx/license-list-data/actions" href="/spdx/license-list-data/actions">
+ <svg class="octicon octicon-play UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
+ <span data-content="Actions">Actions</span>
+ <span title="Not available" class="Counter "></span>
+</a> </li>
+ <li class="d-flex">
+ <a class="js-selected-navigation-item UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="projects-tab" data-hotkey="g b" data-ga-click="Repository, Navigation click, Projects tab" data-selected-links="repo_projects new_repo_project repo_project /spdx/license-list-data/projects" href="/spdx/license-list-data/projects">
+ <svg class="octicon octicon-project UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
+ <span data-content="Projects">Projects</span>
+ <span title="0" hidden="hidden" class="Counter ">0</span>
+</a> </li>
+ <li class="d-flex">
+ <a class="js-selected-navigation-item UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="security-tab" data-hotkey="g s" data-ga-click="Repository, Navigation click, Security tab" data-selected-links="security overview alerts policy token_scanning code_scanning /spdx/license-list-data/security" href="/spdx/license-list-data/security">
+ <svg class="octicon octicon-shield UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M7.467.133a1.75 1.75 0 011.066 0l5.25 1.68A1.75 1.75 0 0115 3.48V7c0 1.566-.32 3.182-1.303 4.682-.983 1.498-2.585 2.813-5.032 3.855a1.7 1.7 0 01-1.33 0c-2.447-1.042-4.049-2.357-5.032-3.855C1.32 10.182 1 8.566 1 7V3.48a1.75 1.75 0 011.217-1.667l5.25-1.68zm.61 1.429a.25.25 0 00-.153 0l-5.25 1.68a.25.25 0 00-.174.238V7c0 1.358.275 2.666 1.057 3.86.784 1.194 2.121 2.34 4.366 3.297a.2.2 0 00.154 0c2.245-.956 3.582-2.104 4.366-3.298C13.225 9.666 13.5 8.36 13.5 7V3.48a.25.25 0 00-.174-.237l-5.25-1.68zM9 10.5a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.75a.75.75 0 10-1.5 0v3a.75.75 0 001.5 0v-3z"></path></svg>
+ <span data-content="Security">Security</span>
+ <include-fragment src="/spdx/license-list-data/security/overall-count" accept="text/fragment+html"></include-fragment>
+</a> </li>
+ <li class="d-flex">
+ <a class="js-selected-navigation-item UnderlineNav-item hx_underlinenav-item no-wrap js-responsive-underlinenav-item" data-tab-item="insights-tab" data-ga-click="Repository, Navigation click, Insights tab" data-selected-links="repo_graphs repo_contributors dependency_graph dependabot_updates pulse people /spdx/license-list-data/pulse" href="/spdx/license-list-data/pulse">
+ <svg class="octicon octicon-graph UnderlineNav-octicon d-none d-sm-inline" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
+ <span data-content="Insights">Insights</span>
+ <span title="Not available" class="Counter "></span>
+</a> </li>
+
+</ul> <div class="position-absolute right-0 pr-3 pr-md-4 pr-lg-5 js-responsive-underlinenav-overflow" style="visibility:hidden;">
+ <details class="details-overlay details-reset position-relative">
+ <summary role="button">
+ <div class="UnderlineNav-item mr-0 border-0">
+ <svg class="octicon octicon-kebab-horizontal" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>
+ <span class="sr-only">More</span>
+ </div>
+</summary> <div>
+ <details-menu role="menu" class="dropdown-menu dropdown-menu-sw ">
+
+ <ul>
+ <li data-menu-item="code-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data" href="/spdx/license-list-data">
+ Code
+</a> </li>
+ <li data-menu-item="pull-requests-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data/pulls" href="/spdx/license-list-data/pulls">
+ Pull requests
+</a> </li>
+ <li data-menu-item="actions-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data/actions" href="/spdx/license-list-data/actions">
+ Actions
+</a> </li>
+ <li data-menu-item="projects-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data/projects" href="/spdx/license-list-data/projects">
+ Projects
+</a> </li>
+ <li data-menu-item="security-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data/security" href="/spdx/license-list-data/security">
+ Security
+</a> </li>
+ <li data-menu-item="insights-tab" hidden>
+ <a role="menuitem" class="js-selected-navigation-item dropdown-item" data-selected-links=" /spdx/license-list-data/pulse" href="/spdx/license-list-data/pulse">
+ Insights
+</a> </li>
+ </ul>
+
+</details-menu>
+</div></details> </div>
+
+</nav>
+ </div>
+
+
+<div class="container-xl clearfix new-discussion-timeline px-3 px-md-4 px-lg-5">
+ <div class="repository-content " >
+
+
+
+
+
+
+ <a class="d-none js-permalink-shortcut" data-hotkey="y" href="/spdx/license-list-data/blob/2e20899c0504ff6c0acfcc1b0994d7163ce46939/text/CC0-1.0.txt">Permalink</a>
+
+ <!-- blob contrib key: blob_contributors:v22:60904d55b299c69f006ee8706a7d208a646abedc37080bdf5043d70f57b2dd97 -->
+ <signup-prompt class="signup-prompt-bg rounded-1" data-prompt="signup" data-optimizely-experiment="signup_prompt_launchpad" hidden>
+ <div class="signup-prompt p-4 text-center mb-4 rounded-1" data-optimizely-variation="control">
+ <div class="position-relative">
+ <button
+ type="button"
+ class="position-absolute top-0 right-0 btn-link link-gray"
+ data-action="click:signup-prompt#dismiss"
+ data-ga-click="(Logged out) Sign up prompt, clicked Dismiss, text:dismiss"
+ data-optimizely-event="click.dismiss_signup_prompt, 648296047.1606502883"
+ >
+ Dismiss
+ </button>
+ <h3 class="pt-2">Join GitHub today</h3>
+ <p class="col-6 mx-auto">GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.</p>
+ <a class="btn btn-primary" data-ga-click="(Logged out) Sign up prompt, clicked Sign up, text:sign-up" data-optimizely-event="click.signup, 648296047.1606502883" data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;files signup prompt&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;SIGN_UP&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="92a85d1a9d7f48aabe31f25f1688dc6e1bee50c0d1c73e2852e7c73824afc4de" href="/join?source=prompt-blob-show&amp;source_repo=spdx%2Flicense-list-data">Sign up</a>
+ </div>
+ </div>
+
+ <div class="position-relative p-4 p-lg-5 mb-4" data-optimizely-variation="launchpad" hidden>
+ <div class="d-sm-flex">
+ <div class="col-sm-8 col-md-9 col-lg-8">
+ <h3 class="h2 lh-condensed mt-sm-1 mt-lg-0">GitHub is where the world builds software</h3>
+ <p class="f4 text-gray mt-2 mb-3">Millions of developers and companies build, ship, and maintain their software on GitHub &#8212; the largest and most advanced development platform in the world.</p>
+ <div class="d-flex flex-items-center pb-3">
+ <a class="btn btn-primary" data-ga-click="(Logged out) Sign up prompt, clicked Sign up, text:sign-up" data-optimizely-event="click.signup, 648296047.1606502883" data-hydro-click="{&quot;event_type&quot;:&quot;authentication.click&quot;,&quot;payload&quot;:{&quot;location_in_page&quot;:&quot;files signup prompt&quot;,&quot;repository_id&quot;:null,&quot;auth_type&quot;:&quot;SIGN_UP&quot;,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="92a85d1a9d7f48aabe31f25f1688dc6e1bee50c0d1c73e2852e7c73824afc4de" href="/join?source=prompt-blob-show&amp;source_repo=spdx%2Flicense-list-data">Sign up for free</a>
+ <button
+ type="button"
+ class="btn-link link-gray ml-3"
+ data-action="click:signup-prompt#dismiss"
+ data-ga-click="(Logged out) Sign up prompt, clicked Dismiss, text:dismiss"
+ data-optimizely-event="click.dismiss_signup_prompt, 648296047.1606502883"
+ >
+ Dismiss
+ </button>
+ </div>
+ </div>
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" preserveAspectRatio="none" viewBox="0 0 1680 40" class="d-none d-sm-block position-absolute width-full" style="bottom: -1px;"><path d="M0 40h1680V30S1340 0 840 0 0 30 0 30z" fill="#fff"></path></svg>
+
+ <div class="d-none d-sm-block position-absolute col-5 col-md-4 col-lg-3 bottom-0 right-0 mr-lg-5 mb-md-n4">
+ <div class="width-full" >
+ <svg viewBox="0 0 300 305" class="width-fit">
+ <defs>
+ <mask id="https___github_githubassets_com_images_modules_site_home_astro-mona-alpha_jpg">
+ <image width="300" height="305" href="https://github.githubassets.com/images/modules/site/home/astro-mona-alpha.jpg"></image>
+ </mask>
+ </defs>
+ <image mask="url(#https___github_githubassets_com_images_modules_site_home_astro-mona-alpha_jpg)" width="300" height="305" href="https://github.githubassets.com/images/modules/site/home/astro-mona.jpg"></image>
+</svg>
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </signup-prompt>
+
+
+ <div class="d-flex flex-items-start flex-shrink-0 pb-3 flex-wrap flex-md-nowrap flex-justify-between flex-md-justify-start">
+
+<div class="position-relative">
+ <details class="details-reset details-overlay mr-0 mb-0 " id="branch-select-menu">
+ <summary class="btn css-truncate"
+ data-hotkey="w"
+ title="Switch branches or tags">
+ <svg class="octicon octicon-git-branch text-gray" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"></path></svg>
+ <span class="css-truncate-target" data-menu-button>master</span>
+ <span class="dropdown-caret"></span>
+ </summary>
+
+ <details-menu class="SelectMenu SelectMenu--hasFilter" src="/spdx/license-list-data/refs/master/text/CC0-1.0.txt?source_action=show&amp;source_controller=blob" preload>
+ <div class="SelectMenu-modal">
+ <include-fragment class="SelectMenu-loading" aria-label="Menu is loading">
+ <svg class="octicon octicon-octoface anim-pulse" height="32" viewBox="0 0 24 24" version="1.1" width="32" aria-hidden="true"><path d="M7.75 11c-.69 0-1.25.56-1.25 1.25v1.5a1.25 1.25 0 102.5 0v-1.5C9 11.56 8.44 11 7.75 11zm1.27 4.5a.469.469 0 01.48-.5h5a.47.47 0 01.48.5c-.116 1.316-.759 2.5-2.98 2.5s-2.864-1.184-2.98-2.5zm7.23-4.5c-.69 0-1.25.56-1.25 1.25v1.5a1.25 1.25 0 102.5 0v-1.5c0-.69-.56-1.25-1.25-1.25z"></path><path fill-rule="evenodd" d="M21.255 3.82a1.725 1.725 0 00-2.141-1.195c-.557.16-1.406.44-2.264.866-.78.386-1.647.93-2.293 1.677A18.442 18.442 0 0012 5c-.93 0-1.784.059-2.569.17-.645-.74-1.505-1.28-2.28-1.664a13.876 13.876 0 00-2.265-.866 1.725 1.725 0 00-2.141 1.196 23.645 23.645 0 00-.69 3.292c-.125.97-.191 2.07-.066 3.112C1.254 11.882 1 13.734 1 15.527 1 19.915 3.13 23 12 23c8.87 0 11-3.053 11-7.473 0-1.794-.255-3.647-.99-5.29.127-1.046.06-2.15-.066-3.125a23.652 23.652 0 00-.689-3.292zM20.5 14c.5 3.5-1.5 6.5-8.5 6.5s-9-3-8.5-6.5c.583-4 3-6 8.5-6s7.928 2 8.5 6z"></path></svg>
+ </include-fragment>
+ </div>
+ </details-menu>
+ </details>
+
+</div>
+
+ <h2 id="blob-path" class="breadcrumb flex-auto min-width-0 text-normal mx-0 mx-md-3 width-full width-md-auto flex-order-1 flex-md-order-none mt-3 mt-md-0">
+ <span class="js-repo-root text-bold"><span class="js-path-segment d-inline-block wb-break-all"><a data-pjax="true" href="/spdx/license-list-data"><span>license-list-data</span></a></span></span><span class="separator">/</span><span class="js-path-segment d-inline-block wb-break-all"><a data-pjax="true" href="/spdx/license-list-data/tree/master/text"><span>text</span></a></span><span class="separator">/</span><strong class="final-path">CC0-1.0.txt</strong>
+ </h2>
+ <a href="/spdx/license-list-data/find/master"
+ class="js-pjax-capture-input btn mr-2 d-none d-md-block"
+ data-pjax
+ data-hotkey="t">
+ Go to file
+ </a>
+
+ <details id="blob-more-options-details" class="details-overlay details-reset position-relative">
+ <summary role="button" type="button" class="btn ">
+ <svg aria-label="More options" class="octicon octicon-kebab-horizontal" height="16" viewBox="0 0 16 16" version="1.1" width="16" role="img"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>
+</summary> <div>
+ <ul class="dropdown-menu dropdown-menu-sw">
+ <li class="d-block d-md-none">
+ <a class="dropdown-item d-flex flex-items-baseline" data-hydro-click="{&quot;event_type&quot;:&quot;repository.click&quot;,&quot;payload&quot;:{&quot;target&quot;:&quot;FIND_FILE_BUTTON&quot;,&quot;repository_id&quot;:56354042,&quot;originating_url&quot;:&quot;https://github.com/spdx/license-list-data/blob/master/text/CC0-1.0.txt&quot;,&quot;user_id&quot;:null}}" data-hydro-click-hmac="fbf825d80d36f44d5da7ab64f3e258e6a536cff0152b0052dc93652831db577b" data-ga-click="Repository, find file, location:repo overview" data-hotkey="t" data-pjax="true" href="/spdx/license-list-data/find/master">
+ <span class="flex-auto">Go to file</span>
+ <span class="text-small text-gray" aria-hidden="true">T</span>
+</a> </li>
+ <li data-toggle-for="blob-more-options-details">
+ <button type="button" data-toggle-for="jumpto-line-details-dialog" class="btn-link dropdown-item">
+ <span class="d-flex flex-items-baseline">
+ <span class="flex-auto">Go to line</span>
+ <span class="text-small text-gray" aria-hidden="true">L</span>
+ </span>
+ </button>
+ </li>
+ <li class="dropdown-divider" role="none"></li>
+ <li>
+ <clipboard-copy value="text/CC0-1.0.txt" class="dropdown-item cursor-pointer" data-toggle-for="blob-more-options-details">
+ Copy path
+ </clipboard-copy>
+ </li>
+ </ul>
+</div></details> </div>
+
+
+
+ <div class="Box d-flex flex-column flex-shrink-0 mb-3">
+ <include-fragment src="/spdx/license-list-data/contributors/master/text/CC0-1.0.txt" class="commit-loader">
+ <div class="Box-header Box-header--blue d-flex flex-items-center">
+ <div class="Skeleton avatar avatar-user flex-shrink-0 ml-n1 mr-n1 mt-n1 mb-n1" style="width:24px;height:24px;"></div>
+ <div class="Skeleton Skeleton--text col-5 ml-2">&nbsp;</div>
+ </div>
+
+ <div class="Box-body d-flex flex-items-center" >
+ <div class="Skeleton Skeleton--text col-1">&nbsp;</div>
+ <span class="text-red h6 loader-error">Cannot retrieve contributors at this time</span>
+ </div>
+</include-fragment> </div>
+
+
+
+
+
+
+ <div class="Box mt-3 position-relative
+ ">
+
+<div class="Box-header py-2 d-flex flex-column flex-shrink-0 flex-md-row flex-md-items-center">
+ <div class="text-mono f6 flex-auto pr-3 flex-order-2 flex-md-order-1 mt-2 mt-md-0">
+
+ 121 lines (109 sloc)
+ <span class="file-info-divider"></span>
+ 6.88 KB
+ </div>
+
+ <div class="d-flex py-1 py-md-0 flex-auto flex-order-1 flex-md-order-2 flex-sm-grow-0 flex-justify-between">
+
+ <div class="BtnGroup">
+ <a href="/spdx/license-list-data/raw/master/text/CC0-1.0.txt" id="raw-url" role="button" class="btn btn-sm BtnGroup-item ">Raw</a>
+ <a href="/spdx/license-list-data/blame/master/text/CC0-1.0.txt" data-hotkey="b" role="button" class="btn js-update-url-with-hash btn-sm BtnGroup-item ">Blame</a>
+ </div>
+
+ <div>
+ <a class="btn-octicon tooltipped tooltipped-nw js-remove-unless-platform"
+ data-platforms="windows,mac"
+ href="https://desktop.github.com"
+ aria-label="Open this file in GitHub Desktop"
+ data-ga-click="Repository, open with desktop">
+ <svg class="octicon octicon-device-desktop" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.75 2.5h12.5a.25.25 0 01.25.25v7.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-7.5a.25.25 0 01.25-.25zM14.25 1H1.75A1.75 1.75 0 000 2.75v7.5C0 11.216.784 12 1.75 12h3.727c-.1 1.041-.52 1.872-1.292 2.757A.75.75 0 004.75 16h6.5a.75.75 0 00.565-1.243c-.772-.885-1.193-1.716-1.292-2.757h3.727A1.75 1.75 0 0016 10.25v-7.5A1.75 1.75 0 0014.25 1zM9.018 12H6.982a5.72 5.72 0 01-.765 2.5h3.566a5.72 5.72 0 01-.765-2.5z"></path></svg>
+ </a>
+
+ <a href="/login?return_to=%2Fspdx%2Flicense-list-data%2Fblob%2Fmaster%2Ftext%2FCC0-1.0.txt" class="btn-octicon disabled tooltipped tooltipped-nw"
+ aria-label="You must be signed in to make or propose changes">
+ <svg class="octicon octicon-pencil" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>
+ </a>
+ <a href="/login?return_to=%2Fspdx%2Flicense-list-data%2Fblob%2Fmaster%2Ftext%2FCC0-1.0.txt" class="btn-octicon btn-octicon-danger disabled tooltipped tooltipped-nw"
+ aria-label="You must be signed in to make or propose changes">
+ <svg class="octicon octicon-trashcan" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>
+ </a>
+ </div>
+ </div>
+</div>
+
+
+
+ <div itemprop="text" class="Box-body p-0 blob-wrapper data type-text gist-border-0">
+
+<table class="highlight tab-size js-file-line-container" data-tab-size="8" data-paste-markdown-skip>
+ <tr>
+ <td id="L1" class="blob-num js-line-number" data-line-number="1"></td>
+ <td id="LC1" class="blob-code blob-code-inner js-file-line">Creative Commons Legal Code</td>
+ </tr>
+ <tr>
+ <td id="L2" class="blob-num js-line-number" data-line-number="2"></td>
+ <td id="LC2" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L3" class="blob-num js-line-number" data-line-number="3"></td>
+ <td id="LC3" class="blob-code blob-code-inner js-file-line">CC0 1.0 Universal</td>
+ </tr>
+ <tr>
+ <td id="L4" class="blob-num js-line-number" data-line-number="4"></td>
+ <td id="LC4" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L5" class="blob-num js-line-number" data-line-number="5"></td>
+ <td id="LC5" class="blob-code blob-code-inner js-file-line"> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE</td>
+ </tr>
+ <tr>
+ <td id="L6" class="blob-num js-line-number" data-line-number="6"></td>
+ <td id="LC6" class="blob-code blob-code-inner js-file-line"> LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN</td>
+ </tr>
+ <tr>
+ <td id="L7" class="blob-num js-line-number" data-line-number="7"></td>
+ <td id="LC7" class="blob-code blob-code-inner js-file-line"> ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS</td>
+ </tr>
+ <tr>
+ <td id="L8" class="blob-num js-line-number" data-line-number="8"></td>
+ <td id="LC8" class="blob-code blob-code-inner js-file-line"> INFORMATION ON AN &quot;AS-IS&quot; BASIS. CREATIVE COMMONS MAKES NO WARRANTIES</td>
+ </tr>
+ <tr>
+ <td id="L9" class="blob-num js-line-number" data-line-number="9"></td>
+ <td id="LC9" class="blob-code blob-code-inner js-file-line"> REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS</td>
+ </tr>
+ <tr>
+ <td id="L10" class="blob-num js-line-number" data-line-number="10"></td>
+ <td id="LC10" class="blob-code blob-code-inner js-file-line"> PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM</td>
+ </tr>
+ <tr>
+ <td id="L11" class="blob-num js-line-number" data-line-number="11"></td>
+ <td id="LC11" class="blob-code blob-code-inner js-file-line"> THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED</td>
+ </tr>
+ <tr>
+ <td id="L12" class="blob-num js-line-number" data-line-number="12"></td>
+ <td id="LC12" class="blob-code blob-code-inner js-file-line"> HEREUNDER.</td>
+ </tr>
+ <tr>
+ <td id="L13" class="blob-num js-line-number" data-line-number="13"></td>
+ <td id="LC13" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L14" class="blob-num js-line-number" data-line-number="14"></td>
+ <td id="LC14" class="blob-code blob-code-inner js-file-line">Statement of Purpose</td>
+ </tr>
+ <tr>
+ <td id="L15" class="blob-num js-line-number" data-line-number="15"></td>
+ <td id="LC15" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L16" class="blob-num js-line-number" data-line-number="16"></td>
+ <td id="LC16" class="blob-code blob-code-inner js-file-line">The laws of most jurisdictions throughout the world automatically confer</td>
+ </tr>
+ <tr>
+ <td id="L17" class="blob-num js-line-number" data-line-number="17"></td>
+ <td id="LC17" class="blob-code blob-code-inner js-file-line">exclusive Copyright and Related Rights (defined below) upon the creator</td>
+ </tr>
+ <tr>
+ <td id="L18" class="blob-num js-line-number" data-line-number="18"></td>
+ <td id="LC18" class="blob-code blob-code-inner js-file-line">and subsequent owner(s) (each and all, an &quot;owner&quot;) of an original work of</td>
+ </tr>
+ <tr>
+ <td id="L19" class="blob-num js-line-number" data-line-number="19"></td>
+ <td id="LC19" class="blob-code blob-code-inner js-file-line">authorship and/or a database (each, a &quot;Work&quot;).</td>
+ </tr>
+ <tr>
+ <td id="L20" class="blob-num js-line-number" data-line-number="20"></td>
+ <td id="LC20" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L21" class="blob-num js-line-number" data-line-number="21"></td>
+ <td id="LC21" class="blob-code blob-code-inner js-file-line">Certain owners wish to permanently relinquish those rights to a Work for</td>
+ </tr>
+ <tr>
+ <td id="L22" class="blob-num js-line-number" data-line-number="22"></td>
+ <td id="LC22" class="blob-code blob-code-inner js-file-line">the purpose of contributing to a commons of creative, cultural and</td>
+ </tr>
+ <tr>
+ <td id="L23" class="blob-num js-line-number" data-line-number="23"></td>
+ <td id="LC23" class="blob-code blob-code-inner js-file-line">scientific works (&quot;Commons&quot;) that the public can reliably and without fear</td>
+ </tr>
+ <tr>
+ <td id="L24" class="blob-num js-line-number" data-line-number="24"></td>
+ <td id="LC24" class="blob-code blob-code-inner js-file-line">of later claims of infringement build upon, modify, incorporate in other</td>
+ </tr>
+ <tr>
+ <td id="L25" class="blob-num js-line-number" data-line-number="25"></td>
+ <td id="LC25" class="blob-code blob-code-inner js-file-line">works, reuse and redistribute as freely as possible in any form whatsoever</td>
+ </tr>
+ <tr>
+ <td id="L26" class="blob-num js-line-number" data-line-number="26"></td>
+ <td id="LC26" class="blob-code blob-code-inner js-file-line">and for any purposes, including without limitation commercial purposes.</td>
+ </tr>
+ <tr>
+ <td id="L27" class="blob-num js-line-number" data-line-number="27"></td>
+ <td id="LC27" class="blob-code blob-code-inner js-file-line">These owners may contribute to the Commons to promote the ideal of a free</td>
+ </tr>
+ <tr>
+ <td id="L28" class="blob-num js-line-number" data-line-number="28"></td>
+ <td id="LC28" class="blob-code blob-code-inner js-file-line">culture and the further production of creative, cultural and scientific</td>
+ </tr>
+ <tr>
+ <td id="L29" class="blob-num js-line-number" data-line-number="29"></td>
+ <td id="LC29" class="blob-code blob-code-inner js-file-line">works, or to gain reputation or greater distribution for their Work in</td>
+ </tr>
+ <tr>
+ <td id="L30" class="blob-num js-line-number" data-line-number="30"></td>
+ <td id="LC30" class="blob-code blob-code-inner js-file-line">part through the use and efforts of others.</td>
+ </tr>
+ <tr>
+ <td id="L31" class="blob-num js-line-number" data-line-number="31"></td>
+ <td id="LC31" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L32" class="blob-num js-line-number" data-line-number="32"></td>
+ <td id="LC32" class="blob-code blob-code-inner js-file-line">For these and/or other purposes and motivations, and without any</td>
+ </tr>
+ <tr>
+ <td id="L33" class="blob-num js-line-number" data-line-number="33"></td>
+ <td id="LC33" class="blob-code blob-code-inner js-file-line">expectation of additional consideration or compensation, the person</td>
+ </tr>
+ <tr>
+ <td id="L34" class="blob-num js-line-number" data-line-number="34"></td>
+ <td id="LC34" class="blob-code blob-code-inner js-file-line">associating CC0 with a Work (the &quot;Affirmer&quot;), to the extent that he or she</td>
+ </tr>
+ <tr>
+ <td id="L35" class="blob-num js-line-number" data-line-number="35"></td>
+ <td id="LC35" class="blob-code blob-code-inner js-file-line">is an owner of Copyright and Related Rights in the Work, voluntarily</td>
+ </tr>
+ <tr>
+ <td id="L36" class="blob-num js-line-number" data-line-number="36"></td>
+ <td id="LC36" class="blob-code blob-code-inner js-file-line">elects to apply CC0 to the Work and publicly distribute the Work under its</td>
+ </tr>
+ <tr>
+ <td id="L37" class="blob-num js-line-number" data-line-number="37"></td>
+ <td id="LC37" class="blob-code blob-code-inner js-file-line">terms, with knowledge of his or her Copyright and Related Rights in the</td>
+ </tr>
+ <tr>
+ <td id="L38" class="blob-num js-line-number" data-line-number="38"></td>
+ <td id="LC38" class="blob-code blob-code-inner js-file-line">Work and the meaning and intended legal effect of CC0 on those rights.</td>
+ </tr>
+ <tr>
+ <td id="L39" class="blob-num js-line-number" data-line-number="39"></td>
+ <td id="LC39" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L40" class="blob-num js-line-number" data-line-number="40"></td>
+ <td id="LC40" class="blob-code blob-code-inner js-file-line">1. Copyright and Related Rights. A Work made available under CC0 may be</td>
+ </tr>
+ <tr>
+ <td id="L41" class="blob-num js-line-number" data-line-number="41"></td>
+ <td id="LC41" class="blob-code blob-code-inner js-file-line">protected by copyright and related or neighboring rights (&quot;Copyright and</td>
+ </tr>
+ <tr>
+ <td id="L42" class="blob-num js-line-number" data-line-number="42"></td>
+ <td id="LC42" class="blob-code blob-code-inner js-file-line">Related Rights&quot;). Copyright and Related Rights include, but are not</td>
+ </tr>
+ <tr>
+ <td id="L43" class="blob-num js-line-number" data-line-number="43"></td>
+ <td id="LC43" class="blob-code blob-code-inner js-file-line">limited to, the following:</td>
+ </tr>
+ <tr>
+ <td id="L44" class="blob-num js-line-number" data-line-number="44"></td>
+ <td id="LC44" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L45" class="blob-num js-line-number" data-line-number="45"></td>
+ <td id="LC45" class="blob-code blob-code-inner js-file-line"> i. the right to reproduce, adapt, distribute, perform, display,</td>
+ </tr>
+ <tr>
+ <td id="L46" class="blob-num js-line-number" data-line-number="46"></td>
+ <td id="LC46" class="blob-code blob-code-inner js-file-line"> communicate, and translate a Work;</td>
+ </tr>
+ <tr>
+ <td id="L47" class="blob-num js-line-number" data-line-number="47"></td>
+ <td id="LC47" class="blob-code blob-code-inner js-file-line"> ii. moral rights retained by the original author(s) and/or performer(s);</td>
+ </tr>
+ <tr>
+ <td id="L48" class="blob-num js-line-number" data-line-number="48"></td>
+ <td id="LC48" class="blob-code blob-code-inner js-file-line">iii. publicity and privacy rights pertaining to a person&#39;s image or</td>
+ </tr>
+ <tr>
+ <td id="L49" class="blob-num js-line-number" data-line-number="49"></td>
+ <td id="LC49" class="blob-code blob-code-inner js-file-line"> likeness depicted in a Work;</td>
+ </tr>
+ <tr>
+ <td id="L50" class="blob-num js-line-number" data-line-number="50"></td>
+ <td id="LC50" class="blob-code blob-code-inner js-file-line"> iv. rights protecting against unfair competition in regards to a Work,</td>
+ </tr>
+ <tr>
+ <td id="L51" class="blob-num js-line-number" data-line-number="51"></td>
+ <td id="LC51" class="blob-code blob-code-inner js-file-line"> subject to the limitations in paragraph 4(a), below;</td>
+ </tr>
+ <tr>
+ <td id="L52" class="blob-num js-line-number" data-line-number="52"></td>
+ <td id="LC52" class="blob-code blob-code-inner js-file-line"> v. rights protecting the extraction, dissemination, use and reuse of data</td>
+ </tr>
+ <tr>
+ <td id="L53" class="blob-num js-line-number" data-line-number="53"></td>
+ <td id="LC53" class="blob-code blob-code-inner js-file-line"> in a Work;</td>
+ </tr>
+ <tr>
+ <td id="L54" class="blob-num js-line-number" data-line-number="54"></td>
+ <td id="LC54" class="blob-code blob-code-inner js-file-line"> vi. database rights (such as those arising under Directive 96/9/EC of the</td>
+ </tr>
+ <tr>
+ <td id="L55" class="blob-num js-line-number" data-line-number="55"></td>
+ <td id="LC55" class="blob-code blob-code-inner js-file-line"> European Parliament and of the Council of 11 March 1996 on the legal</td>
+ </tr>
+ <tr>
+ <td id="L56" class="blob-num js-line-number" data-line-number="56"></td>
+ <td id="LC56" class="blob-code blob-code-inner js-file-line"> protection of databases, and under any national implementation</td>
+ </tr>
+ <tr>
+ <td id="L57" class="blob-num js-line-number" data-line-number="57"></td>
+ <td id="LC57" class="blob-code blob-code-inner js-file-line"> thereof, including any amended or successor version of such</td>
+ </tr>
+ <tr>
+ <td id="L58" class="blob-num js-line-number" data-line-number="58"></td>
+ <td id="LC58" class="blob-code blob-code-inner js-file-line"> directive); and</td>
+ </tr>
+ <tr>
+ <td id="L59" class="blob-num js-line-number" data-line-number="59"></td>
+ <td id="LC59" class="blob-code blob-code-inner js-file-line">vii. other similar, equivalent or corresponding rights throughout the</td>
+ </tr>
+ <tr>
+ <td id="L60" class="blob-num js-line-number" data-line-number="60"></td>
+ <td id="LC60" class="blob-code blob-code-inner js-file-line"> world based on applicable law or treaty, and any national</td>
+ </tr>
+ <tr>
+ <td id="L61" class="blob-num js-line-number" data-line-number="61"></td>
+ <td id="LC61" class="blob-code blob-code-inner js-file-line"> implementations thereof.</td>
+ </tr>
+ <tr>
+ <td id="L62" class="blob-num js-line-number" data-line-number="62"></td>
+ <td id="LC62" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L63" class="blob-num js-line-number" data-line-number="63"></td>
+ <td id="LC63" class="blob-code blob-code-inner js-file-line">2. Waiver. To the greatest extent permitted by, but not in contravention</td>
+ </tr>
+ <tr>
+ <td id="L64" class="blob-num js-line-number" data-line-number="64"></td>
+ <td id="LC64" class="blob-code blob-code-inner js-file-line">of, applicable law, Affirmer hereby overtly, fully, permanently,</td>
+ </tr>
+ <tr>
+ <td id="L65" class="blob-num js-line-number" data-line-number="65"></td>
+ <td id="LC65" class="blob-code blob-code-inner js-file-line">irrevocably and unconditionally waives, abandons, and surrenders all of</td>
+ </tr>
+ <tr>
+ <td id="L66" class="blob-num js-line-number" data-line-number="66"></td>
+ <td id="LC66" class="blob-code blob-code-inner js-file-line">Affirmer&#39;s Copyright and Related Rights and associated claims and causes</td>
+ </tr>
+ <tr>
+ <td id="L67" class="blob-num js-line-number" data-line-number="67"></td>
+ <td id="LC67" class="blob-code blob-code-inner js-file-line">of action, whether now known or unknown (including existing as well as</td>
+ </tr>
+ <tr>
+ <td id="L68" class="blob-num js-line-number" data-line-number="68"></td>
+ <td id="LC68" class="blob-code blob-code-inner js-file-line">future claims and causes of action), in the Work (i) in all territories</td>
+ </tr>
+ <tr>
+ <td id="L69" class="blob-num js-line-number" data-line-number="69"></td>
+ <td id="LC69" class="blob-code blob-code-inner js-file-line">worldwide, (ii) for the maximum duration provided by applicable law or</td>
+ </tr>
+ <tr>
+ <td id="L70" class="blob-num js-line-number" data-line-number="70"></td>
+ <td id="LC70" class="blob-code blob-code-inner js-file-line">treaty (including future time extensions), (iii) in any current or future</td>
+ </tr>
+ <tr>
+ <td id="L71" class="blob-num js-line-number" data-line-number="71"></td>
+ <td id="LC71" class="blob-code blob-code-inner js-file-line">medium and for any number of copies, and (iv) for any purpose whatsoever,</td>
+ </tr>
+ <tr>
+ <td id="L72" class="blob-num js-line-number" data-line-number="72"></td>
+ <td id="LC72" class="blob-code blob-code-inner js-file-line">including without limitation commercial, advertising or promotional</td>
+ </tr>
+ <tr>
+ <td id="L73" class="blob-num js-line-number" data-line-number="73"></td>
+ <td id="LC73" class="blob-code blob-code-inner js-file-line">purposes (the &quot;Waiver&quot;). Affirmer makes the Waiver for the benefit of each</td>
+ </tr>
+ <tr>
+ <td id="L74" class="blob-num js-line-number" data-line-number="74"></td>
+ <td id="LC74" class="blob-code blob-code-inner js-file-line">member of the public at large and to the detriment of Affirmer&#39;s heirs and</td>
+ </tr>
+ <tr>
+ <td id="L75" class="blob-num js-line-number" data-line-number="75"></td>
+ <td id="LC75" class="blob-code blob-code-inner js-file-line">successors, fully intending that such Waiver shall not be subject to</td>
+ </tr>
+ <tr>
+ <td id="L76" class="blob-num js-line-number" data-line-number="76"></td>
+ <td id="LC76" class="blob-code blob-code-inner js-file-line">revocation, rescission, cancellation, termination, or any other legal or</td>
+ </tr>
+ <tr>
+ <td id="L77" class="blob-num js-line-number" data-line-number="77"></td>
+ <td id="LC77" class="blob-code blob-code-inner js-file-line">equitable action to disrupt the quiet enjoyment of the Work by the public</td>
+ </tr>
+ <tr>
+ <td id="L78" class="blob-num js-line-number" data-line-number="78"></td>
+ <td id="LC78" class="blob-code blob-code-inner js-file-line">as contemplated by Affirmer&#39;s express Statement of Purpose.</td>
+ </tr>
+ <tr>
+ <td id="L79" class="blob-num js-line-number" data-line-number="79"></td>
+ <td id="LC79" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L80" class="blob-num js-line-number" data-line-number="80"></td>
+ <td id="LC80" class="blob-code blob-code-inner js-file-line">3. Public License Fallback. Should any part of the Waiver for any reason</td>
+ </tr>
+ <tr>
+ <td id="L81" class="blob-num js-line-number" data-line-number="81"></td>
+ <td id="LC81" class="blob-code blob-code-inner js-file-line">be judged legally invalid or ineffective under applicable law, then the</td>
+ </tr>
+ <tr>
+ <td id="L82" class="blob-num js-line-number" data-line-number="82"></td>
+ <td id="LC82" class="blob-code blob-code-inner js-file-line">Waiver shall be preserved to the maximum extent permitted taking into</td>
+ </tr>
+ <tr>
+ <td id="L83" class="blob-num js-line-number" data-line-number="83"></td>
+ <td id="LC83" class="blob-code blob-code-inner js-file-line">account Affirmer&#39;s express Statement of Purpose. In addition, to the</td>
+ </tr>
+ <tr>
+ <td id="L84" class="blob-num js-line-number" data-line-number="84"></td>
+ <td id="LC84" class="blob-code blob-code-inner js-file-line">extent the Waiver is so judged Affirmer hereby grants to each affected</td>
+ </tr>
+ <tr>
+ <td id="L85" class="blob-num js-line-number" data-line-number="85"></td>
+ <td id="LC85" class="blob-code blob-code-inner js-file-line">person a royalty-free, non transferable, non sublicensable, non exclusive,</td>
+ </tr>
+ <tr>
+ <td id="L86" class="blob-num js-line-number" data-line-number="86"></td>
+ <td id="LC86" class="blob-code blob-code-inner js-file-line">irrevocable and unconditional license to exercise Affirmer&#39;s Copyright and</td>
+ </tr>
+ <tr>
+ <td id="L87" class="blob-num js-line-number" data-line-number="87"></td>
+ <td id="LC87" class="blob-code blob-code-inner js-file-line">Related Rights in the Work (i) in all territories worldwide, (ii) for the</td>
+ </tr>
+ <tr>
+ <td id="L88" class="blob-num js-line-number" data-line-number="88"></td>
+ <td id="LC88" class="blob-code blob-code-inner js-file-line">maximum duration provided by applicable law or treaty (including future</td>
+ </tr>
+ <tr>
+ <td id="L89" class="blob-num js-line-number" data-line-number="89"></td>
+ <td id="LC89" class="blob-code blob-code-inner js-file-line">time extensions), (iii) in any current or future medium and for any number</td>
+ </tr>
+ <tr>
+ <td id="L90" class="blob-num js-line-number" data-line-number="90"></td>
+ <td id="LC90" class="blob-code blob-code-inner js-file-line">of copies, and (iv) for any purpose whatsoever, including without</td>
+ </tr>
+ <tr>
+ <td id="L91" class="blob-num js-line-number" data-line-number="91"></td>
+ <td id="LC91" class="blob-code blob-code-inner js-file-line">limitation commercial, advertising or promotional purposes (the</td>
+ </tr>
+ <tr>
+ <td id="L92" class="blob-num js-line-number" data-line-number="92"></td>
+ <td id="LC92" class="blob-code blob-code-inner js-file-line">&quot;License&quot;). The License shall be deemed effective as of the date CC0 was</td>
+ </tr>
+ <tr>
+ <td id="L93" class="blob-num js-line-number" data-line-number="93"></td>
+ <td id="LC93" class="blob-code blob-code-inner js-file-line">applied by Affirmer to the Work. Should any part of the License for any</td>
+ </tr>
+ <tr>
+ <td id="L94" class="blob-num js-line-number" data-line-number="94"></td>
+ <td id="LC94" class="blob-code blob-code-inner js-file-line">reason be judged legally invalid or ineffective under applicable law, such</td>
+ </tr>
+ <tr>
+ <td id="L95" class="blob-num js-line-number" data-line-number="95"></td>
+ <td id="LC95" class="blob-code blob-code-inner js-file-line">partial invalidity or ineffectiveness shall not invalidate the remainder</td>
+ </tr>
+ <tr>
+ <td id="L96" class="blob-num js-line-number" data-line-number="96"></td>
+ <td id="LC96" class="blob-code blob-code-inner js-file-line">of the License, and in such case Affirmer hereby affirms that he or she</td>
+ </tr>
+ <tr>
+ <td id="L97" class="blob-num js-line-number" data-line-number="97"></td>
+ <td id="LC97" class="blob-code blob-code-inner js-file-line">will not (i) exercise any of his or her remaining Copyright and Related</td>
+ </tr>
+ <tr>
+ <td id="L98" class="blob-num js-line-number" data-line-number="98"></td>
+ <td id="LC98" class="blob-code blob-code-inner js-file-line">Rights in the Work or (ii) assert any associated claims and causes of</td>
+ </tr>
+ <tr>
+ <td id="L99" class="blob-num js-line-number" data-line-number="99"></td>
+ <td id="LC99" class="blob-code blob-code-inner js-file-line">action with respect to the Work, in either case contrary to Affirmer&#39;s</td>
+ </tr>
+ <tr>
+ <td id="L100" class="blob-num js-line-number" data-line-number="100"></td>
+ <td id="LC100" class="blob-code blob-code-inner js-file-line">express Statement of Purpose.</td>
+ </tr>
+ <tr>
+ <td id="L101" class="blob-num js-line-number" data-line-number="101"></td>
+ <td id="LC101" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L102" class="blob-num js-line-number" data-line-number="102"></td>
+ <td id="LC102" class="blob-code blob-code-inner js-file-line">4. Limitations and Disclaimers.</td>
+ </tr>
+ <tr>
+ <td id="L103" class="blob-num js-line-number" data-line-number="103"></td>
+ <td id="LC103" class="blob-code blob-code-inner js-file-line">
+</td>
+ </tr>
+ <tr>
+ <td id="L104" class="blob-num js-line-number" data-line-number="104"></td>
+ <td id="LC104" class="blob-code blob-code-inner js-file-line"> a. No trademark or patent rights held by Affirmer are waived, abandoned,</td>
+ </tr>
+ <tr>
+ <td id="L105" class="blob-num js-line-number" data-line-number="105"></td>
+ <td id="LC105" class="blob-code blob-code-inner js-file-line"> surrendered, licensed or otherwise affected by this document.</td>
+ </tr>
+ <tr>
+ <td id="L106" class="blob-num js-line-number" data-line-number="106"></td>
+ <td id="LC106" class="blob-code blob-code-inner js-file-line"> b. Affirmer offers the Work as-is and makes no representations or</td>
+ </tr>
+ <tr>
+ <td id="L107" class="blob-num js-line-number" data-line-number="107"></td>
+ <td id="LC107" class="blob-code blob-code-inner js-file-line"> warranties of any kind concerning the Work, express, implied,</td>
+ </tr>
+ <tr>
+ <td id="L108" class="blob-num js-line-number" data-line-number="108"></td>
+ <td id="LC108" class="blob-code blob-code-inner js-file-line"> statutory or otherwise, including without limitation warranties of</td>
+ </tr>
+ <tr>
+ <td id="L109" class="blob-num js-line-number" data-line-number="109"></td>
+ <td id="LC109" class="blob-code blob-code-inner js-file-line"> title, merchantability, fitness for a particular purpose, non</td>
+ </tr>
+ <tr>
+ <td id="L110" class="blob-num js-line-number" data-line-number="110"></td>
+ <td id="LC110" class="blob-code blob-code-inner js-file-line"> infringement, or the absence of latent or other defects, accuracy, or</td>
+ </tr>
+ <tr>
+ <td id="L111" class="blob-num js-line-number" data-line-number="111"></td>
+ <td id="LC111" class="blob-code blob-code-inner js-file-line"> the present or absence of errors, whether or not discoverable, all to</td>
+ </tr>
+ <tr>
+ <td id="L112" class="blob-num js-line-number" data-line-number="112"></td>
+ <td id="LC112" class="blob-code blob-code-inner js-file-line"> the greatest extent permissible under applicable law.</td>
+ </tr>
+ <tr>
+ <td id="L113" class="blob-num js-line-number" data-line-number="113"></td>
+ <td id="LC113" class="blob-code blob-code-inner js-file-line"> c. Affirmer disclaims responsibility for clearing rights of other persons</td>
+ </tr>
+ <tr>
+ <td id="L114" class="blob-num js-line-number" data-line-number="114"></td>
+ <td id="LC114" class="blob-code blob-code-inner js-file-line"> that may apply to the Work or any use thereof, including without</td>
+ </tr>
+ <tr>
+ <td id="L115" class="blob-num js-line-number" data-line-number="115"></td>
+ <td id="LC115" class="blob-code blob-code-inner js-file-line"> limitation any person&#39;s Copyright and Related Rights in the Work.</td>
+ </tr>
+ <tr>
+ <td id="L116" class="blob-num js-line-number" data-line-number="116"></td>
+ <td id="LC116" class="blob-code blob-code-inner js-file-line"> Further, Affirmer disclaims responsibility for obtaining any necessary</td>
+ </tr>
+ <tr>
+ <td id="L117" class="blob-num js-line-number" data-line-number="117"></td>
+ <td id="LC117" class="blob-code blob-code-inner js-file-line"> consents, permissions or other rights required for any use of the</td>
+ </tr>
+ <tr>
+ <td id="L118" class="blob-num js-line-number" data-line-number="118"></td>
+ <td id="LC118" class="blob-code blob-code-inner js-file-line"> Work.</td>
+ </tr>
+ <tr>
+ <td id="L119" class="blob-num js-line-number" data-line-number="119"></td>
+ <td id="LC119" class="blob-code blob-code-inner js-file-line"> d. Affirmer understands and acknowledges that Creative Commons is not a</td>
+ </tr>
+ <tr>
+ <td id="L120" class="blob-num js-line-number" data-line-number="120"></td>
+ <td id="LC120" class="blob-code blob-code-inner js-file-line"> party to this document and has no duty or obligation with respect to</td>
+ </tr>
+ <tr>
+ <td id="L121" class="blob-num js-line-number" data-line-number="121"></td>
+ <td id="LC121" class="blob-code blob-code-inner js-file-line"> this CC0 or use of the Work.</td>
+ </tr>
+</table>
+
+ <details class="details-reset details-overlay BlobToolbar position-absolute js-file-line-actions dropdown d-none" aria-hidden="true">
+ <summary class="btn-octicon ml-0 px-2 p-0 bg-white border border-gray-dark rounded-1" aria-label="Inline file action toolbar">
+ <svg class="octicon octicon-kebab-horizontal" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>
+ </summary>
+ <details-menu>
+ <ul class="BlobToolbar-dropdown dropdown-menu dropdown-menu-se mt-2" style="width:185px">
+ <li>
+ <clipboard-copy role="menuitem" class="dropdown-item" id="js-copy-lines" style="cursor:pointer;">
+ Copy lines
+ </clipboard-copy>
+ </li>
+ <li>
+ <clipboard-copy role="menuitem" class="dropdown-item" id="js-copy-permalink" style="cursor:pointer;">
+ Copy permalink
+ </clipboard-copy>
+ </li>
+ <li><a class="dropdown-item js-update-url-with-hash" id="js-view-git-blame" role="menuitem" href="/spdx/license-list-data/blame/2e20899c0504ff6c0acfcc1b0994d7163ce46939/text/CC0-1.0.txt">View git blame</a></li>
+ </ul>
+ </details-menu>
+ </details>
+
+ </div>
+
+ </div>
+
+
+
+
+ <details class="details-reset details-overlay details-overlay-dark" id="jumpto-line-details-dialog">
+ <summary data-hotkey="l" aria-label="Jump to line"></summary>
+ <details-dialog class="Box Box--overlay d-flex flex-column anim-fade-in fast linejump" aria-label="Jump to line">
+ <!-- '"` --><!-- </textarea></xmp> --></option></form><form class="js-jump-to-line-form Box-body d-flex" action="" accept-charset="UTF-8" method="get">
+ <input class="form-control flex-auto mr-3 linejump-input js-jump-to-line-field" type="text" placeholder="Jump to line&hellip;" aria-label="Jump to line" autofocus>
+ <button type="submit" class="btn" data-close-dialog>Go</button>
+</form> </details-dialog>
+ </details>
+
+
+
+
+ </div>
+</div>
+
+ </main>
+ </div>
+
+ </div>
+
+
+<div class="footer container-xl width-full p-responsive" role="contentinfo">
+ <div class="position-relative d-flex flex-row-reverse flex-lg-row flex-wrap flex-lg-nowrap flex-justify-center flex-lg-justify-between flex-sm-items-center pt-6 pb-2 mt-6 f6 text-gray border-top border-gray-light ">
+ <a aria-label="Homepage" title="GitHub" class="footer-octicon d-none d-lg-block mr-lg-4" href="https://github.com">
+ <svg height="24" class="octicon octicon-mark-github" viewBox="0 0 16 16" version="1.1" width="24" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
+</a>
+ <ul class="list-style-none d-flex flex-wrap col-12 flex-justify-center flex-lg-justify-between mb-2 mb-lg-0">
+ <li class="mr-3 mr-lg-0">&copy; 2020 GitHub, Inc.</li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to terms, text:terms" href="https://github.com/site/terms">Terms</a></li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to privacy, text:privacy" href="https://github.com/site/privacy">Privacy</a></li>
+ <li class="js-cookie-consent-preferences-link-container mr-3 mr-lg-0" hidden="hidden">
+ <button data-ga-click="Footer, go to cookie preferences, text:cookie preferences" class="btn-link js-cookie-consent-preferences-link" type="button">Cookie Preferences</button>
+</li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to security, text:security" href="https://github.com/security">Security</a></li>
+ <li class="mr-3 mr-lg-0"><a href="https://githubstatus.com/" data-ga-click="Footer, go to status, text:status">Status</a></li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to help, text:help" href="https://docs.github.com">Help</a></li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to contact, text:contact" href="https://github.com/contact">Contact GitHub</a></li>
+ <li class="mr-3 mr-lg-0"><a href="https://github.com/pricing" data-ga-click="Footer, go to Pricing, text:Pricing">Pricing</a></li>
+ <li class="mr-3 mr-lg-0"><a href="https://docs.github.com" data-ga-click="Footer, go to api, text:api">API</a></li>
+ <li class="mr-3 mr-lg-0"><a href="https://services.github.com" data-ga-click="Footer, go to training, text:training">Training</a></li>
+ <li class="mr-3 mr-lg-0"><a href="https://github.blog" data-ga-click="Footer, go to blog, text:blog">Blog</a></li>
+ <li class="mr-3 mr-lg-0"><a data-ga-click="Footer, go to about, text:about" href="https://github.com/about">About</a></li>
+ </ul>
+ </div>
+ <div class="d-flex flex-justify-center pb-6">
+ <span class="f6 text-gray-light"></span>
+ </div>
+
+
+</div>
+
+
+
+ <div id="ajax-error-message" class="ajax-error-message flash flash-error">
+ <svg class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
+ <button type="button" class="flash-close js-ajax-error-dismiss" aria-label="Dismiss error">
+ <svg class="octicon octicon-x" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>
+ </button>
+ You can’t perform that action at this time.
+ </div>
+
+
+ <div class="js-stale-session-flash flash flash-warn flash-banner" hidden
+ >
+ <svg class="octicon octicon-alert" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
+ <span class="js-stale-session-flash-signed-in" hidden>You signed in with another tab or window. <a href="">Reload</a> to refresh your session.</span>
+ <span class="js-stale-session-flash-signed-out" hidden>You signed out in another tab or window. <a href="">Reload</a> to refresh your session.</span>
+ </div>
+ <template id="site-details-dialog">
+ <details class="details-reset details-overlay details-overlay-dark lh-default text-gray-dark hx_rsm" open>
+ <summary role="button" aria-label="Close dialog"></summary>
+ <details-dialog class="Box Box--overlay d-flex flex-column anim-fade-in fast hx_rsm-dialog hx_rsm-modal">
+ <button class="Box-btn-octicon m-0 btn-octicon position-absolute right-0 top-0" type="button" aria-label="Close dialog" data-close-dialog>
+ <svg class="octicon octicon-x" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>
+ </button>
+ <div class="octocat-spinner my-6 js-details-dialog-spinner"></div>
+ </details-dialog>
+ </details>
+</template>
+
+ <div class="Popover js-hovercard-content position-absolute" style="display: none; outline: none;" tabindex="0">
+ <div class="Popover-message Popover-message--bottom-left Popover-message--large Box box-shadow-large" style="width:360px;">
+ </div>
+</div>
+
+ <div class="js-cookie-consent-banner" hidden>
+ <div class="hx_cookie-banner p-2 p-sm-3 p-md-4">
+ <div style="max-width: 1194px;" class="Box hx_cookie-banner-box box-shadow-medium mx-auto">
+ <div class="Box-body border-0 py-0 px-3 px-md-4">
+ <div class="js-main-cookie-banner hx_cookie-banner-main">
+ <div class="d-md-flex flex-items-center py-3">
+ <p class="f5 flex-1 mb-3 mb-md-0">
+
+ We use <span class="text-bold">optional</span> third-party analytics cookies to understand how you use GitHub.com so we can build better products.
+
+ <span class="btn-link js-cookie-consent-learn-more">Learn more</span>.
+ </p>
+ <div class="d-flex d-md-block flex-wrap flex-sm-nowrap">
+ <button class="btn btn-outline flex-1 mr-1 mx-sm-1 m-md-0 ml-md-2 js-cookie-consent-accept">Accept</button>
+ <button class="btn btn-outline flex-1 ml-1 m-md-0 ml-md-2 js-cookie-consent-reject">Reject</button>
+ </div>
+ </div>
+ </div>
+
+ <div class="js-cookie-details hx_cookie-banner-details" hidden>
+ <div class="d-md-flex flex-items-center py-3">
+ <p class="f5 flex-1 mb-2 mb-md-0">
+
+ We use <span class="text-bold">optional</span> third-party analytics cookies to understand how you use GitHub.com so we can build better products.
+
+ <br>
+ You can always update your selection by clicking <span class="text-bold">Cookie Preferences</span> at the bottom of the page.
+ For more information, see our <a href="https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-privacy-statement">Privacy Statement</a>.
+ </p>
+ </div>
+
+ <div class="d-md-flex flex-items-center py-3 border-top">
+ <div class="f5 flex-1 mb-2 mb-md-0">
+ <h5 class="mb-1">Essential cookies</h5>
+ <p class="f6 mb-md-0">We use essential cookies to perform essential website functions, e.g. they're used to log you in.
+ <a href="https://docs.github.com/en/github/site-policy/github-subprocessors-and-cookies">Learn more</a>
+ </p>
+ </div>
+ <div class="text-right">
+ <h5 class="text-blue">Always active</h5>
+ </div>
+ </div>
+
+ <div class="d-md-flex flex-items-center py-3 border-top">
+ <div class="f5 flex-1 mb-2 mb-md-0">
+ <h5 class="mb-1">Analytics cookies</h5>
+ <p class="f6 mb-md-0">We use analytics cookies to understand how you use our websites so we can make them better, e.g. they're used to gather information about the pages you visit and how many clicks you need to accomplish a task.
+ <a href="https://docs.github.com/en/github/site-policy/github-subprocessors-and-cookies">Learn more</a>
+ </p>
+ </div>
+ <div class="text-right">
+ <div class="BtnGroup mt-1 mt-md-0 ml-2">
+ <button class="btn btn-outline BtnGroup-item js-accept-analytics-cookies" type="button">Accept</button>
+ <button class="btn btn-outline BtnGroup-item js-reject-analytics-cookies" type="button">Reject</button>
+ </div>
+ </div>
+ </div>
+
+ <div class="text-right py-3 border-top">
+ <button class="btn btn-primary js-save-cookie-preferences" type="button" disabled>Save preferences</button>
+ </div>
+ </div>
+</div></div> </div>
+</div>
+
+
+ </body>
+</html>
+
diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt
new file mode 100644
index 0000000..f0fd20a
--- /dev/null
+++ b/LICENSES/MIT.txt
@@ -0,0 +1,20 @@
+MIT License
+
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 3231d31..17b7778 100644
--- a/README.md
+++ b/README.md
@@ -3,36 +3,36 @@ travelynx - Railway Travel Logger
[travelynx](https://finalrewind.org/projects/travelynx/) allows checking into
and out of individual trains, thus providing a log of your railway journeys
-annotated with real-time delays and service messages. At the moment, it only
-supports german railways and trains which are exposed by the Deutsche Bahn
-[IRIS Interface](https://finalrewind.org/projects/Travel-Status-DE-IRIS/).
+annotated with real-time delays and service messages. It supports german
+railways and trains exposed by the Deutsche Bahn [IRIS
+Interface](https://finalrewind.org/projects/Travel-Status-DE-IRIS/) as well as
+local transit and some trains outside of germany exposed by the Deutsche Bahn
+[HAFAS Interface](https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/).
+
+You can use the public instance on [travelynx.de](https://travelynx.de) or
+host your own. See the Installation and Setup notes below.
Dependencies
---
- * perl >= 5.10
- * carton or cpanminus
+ * perl ≥ 5.20
+ * carton
* build-essential
* libpq-dev
* git
-Perl Dependencies
+Installation
---
travelynx depends on a set of Perl modules which are documented in `cpanfile`.
-After installing the dependencies mentioned above, you can use carton or
-cpanminus to install Perl depenencies locally.
+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 either
+In the project root directory (where `cpanfile` resides), run
```
-carton install
-```
-
-or
-
-```
-cpanm --installdeps .
+carton install --deployment
```
and set `PERL5LIB=.../local/lib/perl5` before executing any travelynx
@@ -44,16 +44,18 @@ 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, though
-setup on other distribution should be similar.
+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 configure it
+ (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
@@ -81,11 +83,12 @@ Updating
---
It is recommended to run travelynx directly from the git repository. When
-updating, the workflow depends on whether schema updates need to applied
+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
@@ -97,7 +100,47 @@ fi
```
Note that this is subject to change -- the application may perform schema
-updates automatically in the future.
+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.
+* 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
---
@@ -123,19 +166,17 @@ bar. You will see a list of the five stations closest to your current location
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 now where you're headed, you
+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 any time.
+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, it will not be included in the scheduled and
-real-time data fetched by travelynx. In this case, you have to check out
-without arrival data using the link at the bottom of the checkin menu's station
-list.
+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
---
@@ -150,3 +191,33 @@ 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
+---
+
+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.
+
+The program code of travelynx is licensed under the terms of the GNU AGPL v3.
+HTML Templates and SASS/CSS layout are licensed under the terms of the MIT
+License. This means that you are free to host your own travelynx instance,
+both for personal/internal and public use, under the following conditions.
+
+* You are free to change HTML/SASS/CSS templates as you see fit (though you
+ must not remove the copyright headers).
+* If you make changes to the program code, that is, a file below lib/ or a
+ travelynx javascript file below public/static/js/, you must make those
+ changes available to the public.
+
+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
+---
+
+Mirrors of the travelynx repository are maintained at the following locations:
+
+* [Chaosdorf](https://chaosdorf.de/git/derf/travelynx)
+* [git.finalrewind.org](https://git.finalrewind.org/travelynx/)
+* [GitHub](https://github.com/derf/travelynx)
diff --git a/contrib/i3bar-snippet.py b/contrib/i3bar-snippet.py
new file mode 100755
index 0000000..c02d63c
--- /dev/null
+++ b/contrib/i3bar-snippet.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python3
+
+# This script queries the Travelynx API if you are checked into a train. If
+# yes, bahn.expert is additionally queried for the next stop, and a JSON object
+# like this is written to stdout:
+# {"full_text": "RE26824, next: D\u00fcren at <span fgcolor=\"#ff0000\">15:38+5</span>, dest: Aachen Hbf at <span fgcolor=\"#ff0000\">16:07+5</span>", "markup": "pango"},
+# The script then exits.
+#
+# Configuration:
+# - Place your API key from https://travelynx.de/account at
+# ~/.config/travelynx.conf .
+# - Then integrate into whatever generates your i3bar input.
+# - Make sure you use i3bar with a pango font, so that the color tags are
+# picked up.
+
+
+from datetime import datetime
+import dateutil
+import dateutil.parser
+import json
+from pathlib import Path
+import requests
+import sys
+import xdg # not pyxdg!
+
+
+def format_stop(stop_name, predicted_arrival_timestamp, delay):
+ color = "#ffffff"
+ if delay > 0:
+ if delay <= 2:
+ color = "#ffff00"
+ else:
+ color = "#ff0000"
+ delayStr = "({:+.0f})".format(delay)
+ else:
+ delayStr = ""
+ if isinstance(predicted_arrival_timestamp, int):
+ predicted_arrival_time = datetime.fromtimestamp(predicted_arrival_timestamp)
+ else:
+ # We assume it's datetime already.
+ predicted_arrival_time = predicted_arrival_timestamp
+ return f'{stop_name} at <span fgcolor="{color}">{predicted_arrival_time:%H:%M}{delayStr}</span>'
+
+
+api_key_path = Path(xdg.xdg_config_home(), "travelynx.conf")
+if api_key_path.exists():
+ with api_key_path.open("r") as f:
+ api_key = f.read().strip()
+else:
+ print(
+ f"Could not find Travelynx API key at {api_key_path}.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+
+api_base = f"https://travelynx.de/api/v1/status/{api_key}"
+try:
+ res = requests.get(api_base)
+except requests.exceptions.ConnectionError:
+ print(
+ json.dumps({"full_text": "Could not connect to travelynx", "color": "#ff0000"})
+ + ","
+ )
+ sys.exit()
+
+j = res.json()
+# print(json.dumps(j, sort_keys=True, indent=4), file=sys.stderr)
+
+if not j["checkedIn"]:
+ sys.exit()
+
+out_fields = []
+
+train = "{}{}".format(j["train"]["type"], j["train"]["no"])
+out_fields.append(train)
+destination_name = j["toStation"]["name"]
+scheduled_arrival_timestamp = j["toStation"]["scheduledTime"]
+predicted_arrival_timestamp = j["toStation"]["realTime"]
+delay = (predicted_arrival_timestamp - scheduled_arrival_timestamp) / 60
+
+try:
+ details_res = requests.get(f"https://bahn.expert/api/hafas/v2/details/{train}")
+ details = details_res.json()
+ # print(json.dumps(details, sort_keys=True, indent=4), file=sys.stderr)
+ next_stop_name = details["currentStop"]["station"]["title"]
+ if next_stop_name == destination_name:
+ out_fields.append("next")
+ else:
+ next_predicted_arrival_time = dateutil.parser.isoparse(
+ details["currentStop"]["arrival"]["time"]
+ )
+ next_predicted_arrival_time = next_predicted_arrival_time.astimezone(
+ dateutil.tz.tzlocal()
+ )
+ next_delay = details["currentStop"]["arrival"]["delay"]
+ out_fields.append(
+ "next: "
+ + format_stop(next_stop_name, next_predicted_arrival_time, next_delay)
+ )
+except requests.exceptions.ConnectionError:
+ pass
+
+out_fields.append(
+ "dest: " + format_stop(destination_name, predicted_arrival_timestamp, delay)
+)
+
+s = ", ".join(out_fields)
+
+out_obj = {"full_text": s, "markup": "pango"}
+print(json.dumps(out_obj) + ",")
diff --git a/contrib/polybar.sh b/contrib/polybar.sh
new file mode 100644
index 0000000..df2a523
--- /dev/null
+++ b/contrib/polybar.sh
@@ -0,0 +1,201 @@
+#!/bin/bash
+
+# See <https://github.com/thisjade/TravelynxPolybar/blob/main/README.md>
+# for configuration details
+
+# Interval for refreshing Data and giving it to Polybar
+INTERVAL=1
+
+# Delay Notification Variables
+notificationDelaySent="false"
+notificationLastDelay=0
+notificationNextStopSent="true"
+notificationNextStopTime=""
+
+# Place your API Key here
+API_KEY=
+NOTIFICATIONS_NEXT_STOP="true"
+NOTIFICATIONS_DELAY="true"
+LANGUAGE="DE"
+SYMBOL=""
+
+
+while true; do
+ # curl'ing of all needed Data from https://travelynx.de
+ isCheckedIn=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .checkedIn | sed 's/"//' | sed 's/"//')
+ echo "$isCheckedIn" > /dev/null;
+ trainType=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .train.type | sed 's/"//' | sed 's/"//')
+ echo "$trainType" > /dev/null;
+ trainNo=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .train.no | sed 's/"//' | sed 's/"//')
+ echo "$trainNo" > /dev/null;
+ trainLine=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .train.line | sed 's/"//' | sed 's/"//')
+ echo "$trainLine" > /dev/null;
+ toStation=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .toStation.name | sed 's/"//' | sed 's/"//')
+ echo "$toStation" > /dev/null;
+ arrivalTime=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .toStation.scheduledTime)
+ echo "$arrivalTime" > /dev/null;
+ actualArrivalTime=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .toStation.realTime)
+ echo "$actualArrivalTime" > /dev/null;
+ arrivalTimeDate=$(date +%H:%M -d @$arrivalTime)
+ actualArrivalTimeDate=$(date +%H:%M -d @$actualArrivalTime)
+
+ nextStationTime=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .intermediateStops[].realArrival)
+ nextStationName=$(curl -s https://travelynx.de/api/v1/status/$API_KEY | jq .intermediateStops[].name | sed 's/"//' | sed 's/"//')
+
+
+ SAVEIFS=$IFS
+ IFS=$'\n'
+ nextStationTime=($nextStationTime)
+ nextStationName=($nextStationName)
+ IFS=$SAVEIFS
+
+ if [ "$(date +%H:%M)" != "$notificationNextStopTime" ]
+ then
+ for (( i=0; i<${#nextStationTime[@]}; i++ ))
+ do
+
+ if [ "$(date +%H:%M)" == "$(date +%H:%M -d @${nextStationTime[$i]})" ] && [ "$NOTIFICATIONS_NEXT_STOP" == "true" ] && [ "$LANGUAGE" == "DE" ]
+ then
+ notify-send "Nächster Halt:" "${nextStationName[$i]} um $(date +%H:%M -d @${nextStationTime[$i]})"
+ notificationNextStopTime=$(date +%H:%M -d @${nextStationTime[$i]})
+ fi
+
+ if [ "$(date +%H:%M)" == "$(date +%H:%M -d @${nextStationTime[$i]})" ] && [ "$NOTIFICATIONS_NEXT_STOP" == "true" ] && [ "$LANGUAGE" == "EN" ]
+ then
+ notify-send "Next Stop:" "${nextStationName[$i]} at $(date +%H:%M -d @${nextStationTime[$i]})"
+ notificationNextStopTime=$(date +%H:%M -d @${nextStationTime[$i]})
+ fi
+ done
+ fi
+
+
+ # Checking if Arrival Time changed to send a new Notification
+ if [ "$actualArrivalTime" -gt "$notificationLastDelay" ]
+ then
+ notificationDelaySent="false"
+ fi
+
+ # Checking if Arrival Time changed to send a new Notification
+ if [ "$actualArrivalTime" != "$notificationLastDelay" ] && [ "$actualArrivalTime" != "$arrivalTime" ]
+ then
+ notificationDelaySent="false"
+ fi
+
+
+ # Sending a Notification if an ICE Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType == "ICE" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Zugverspätung:" "Information zu $trainType $trainNo nach $toStation Ankuft heute $actualArrivalTimeDate anstatt $arrivalTimeDate"
+ fi
+
+ # Sending a Notification if an ICE Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType == "ICE" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Delay:" "Information on $trainType $trainNo to $toStation arrival today $actualArrivalTimeDate instead of $arrivalTimeDate"
+ fi
+
+ # Sending a Notification if an IC Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType == "IC" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Zugverspätung:" "Information zu $trainType $trainNo nach $toStation Ankuft heute $actualArrivalTimeDate anstatt $arrivalTimeDate"
+ fi
+
+ # Sending a Notification if an IC Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType == "IC" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Delay:" "Information on $trainType $trainNo to $toStation arrival today $actualArrivalTimeDate instead of $arrivalTimeDate"
+ fi
+
+ # Sending a Notification if other (not ICE/IC) Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Zugverspätung:" "Information zu $trainType $trainLine nach $toStation Ankuft heute $actualArrivalTimeDate anstatt $arrivalTimeDate"
+ fi
+
+
+ # Sending a Notification if other (not ICE/IC) Train is delayed
+ if [ $isCheckedIn = "true" ] && [ "$actualArrivalTime" -gt "$arrivalTime" ] && [ $notificationDelaySent == "false" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $NOTIFICATIONS_DELAY == "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notificationDelaySent="true"
+ notify-send "Delay:" "Information on $trainType $trainLine to $toStation arrival today $actualArrivalTimeDate instead of $arrivalTimeDate"
+ fi
+
+ # Sending a Notification if the ICE Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType == "ICE" ] && [ $NOTIFICATION_DELAY == "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notify-send "Information:" "Information zu $trainType $trainNo nach $toStation ist wieder pünktlich"
+ fi
+
+ # Sending a Notification if the ICE Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType == "ICE" ] && [ $NOTIFICATION_DELAY == "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notify-send "Information:" "Information on $trainType $trainNo to $toStation is on time again"
+ fi
+
+ # Sending a Notification if the IC Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType == "IC" ] && [ $NOTIFICATION_DELAY == "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notify-send "Information:" "Information zu $trainType $trainNo nach $toStation ist wieder pünktlich"
+ fi
+
+ # Sending a Notification if the IC Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType == "IC" ] && [ $NOTIFICATION_DELAY == "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notify-send "Information:" "Information on $trainType $trainNo to $toStation is on time again"
+ fi
+
+ # Sending a Notification if a other (not ICE/IC) Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $NOTIFICATION_DELAY = "true" ] && [ $LANGUAGE == "DE" ]
+ then
+ notify-send "Information:" "Information zu $trainType $trainLine nach $toStation ist wieder pünktlich"
+ fi
+
+ # Sending a Notification if a other (not ICE/IC) Train is on time again
+ if [ "$actualArrivalTime" -eq "$arrivalTime" ] && [ $notificationDelaySent == "true" ] && [ $isCheckedIn == "true" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $NOTIFICATION_DELAY = "true" ] && [ $LANGUAGE == "EN" ]
+ then
+ notify-send "Information:" "Information on $trainType $trainLine ti $toStation is on time again"
+ fi
+
+ # Saving the Delay from the latest Notification
+ notificationLastDelay=$actualArrivalTime
+
+ # Showing the Label for Polybar
+ if [ $isCheckedIn == "true" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $LANGUAGE == "DE" ]
+ then
+ echo "$SYMBOL" $trainType $trainLine "nach" $toStation
+ elif [ $isCheckedIn == "true" ] && [ $trainType != "ICE" ] && [ $trainType != "IC" ] && [ $LANGUAGE == "EN" ]
+ then
+ echo "$SYMBOL" $trainType $trainLine "to" $toStation
+ elif [ $isCheckedIn == "true" ] && [ $trainType == "IC" ] && [ $LANGUAGE == "DE" ]
+ then
+ echo "$SYMBOL" $trainType $trainNo "nach" $toStation
+ elif [ $isCheckedIn == "true" ] && [ $trainType == "IC" ] && [ $LANGUAGE == "EN" ]
+ then
+ echo "$SYMBOL" $trainType $trainNo "to" $toStation
+ elif [ $isCheckedIn == "true" ] && [ $trainType == "ICE" ] && [ $LANGUAGE == "DE" ]
+ then
+ echo "$SYMBOL" $trainType $trainNo "nach" $toStation
+ elif [ $isCheckedIn == "true" ] && [ $trainType == "ICE" ] && [ $LANGUAGE == "EN" ]
+ then
+ echo "$SYMBOL" $trainType $trainNo "to" $toStation
+ elif [ $isCheckedIn == "false" ] && [ $LANGUAGE == "EN" ]
+ then
+ echo "$SYMBOL"" not checked in"
+ notificationDelaySent="false"
+ notificationLastDelay=0
+ notificationNextStopSent="true"
+ else
+ echo "$SYMBOL"" nicht eingecheckt"
+ notificationDelaySent="false"
+ notificationLastDelay=0
+ notificationNextStopSent="true"
+ notificationNextStopTime=""
+ fi
+ sleep $INTERVAL
+
+done
diff --git a/cpanfile b/cpanfile
index 7a869c5..f81887c 100644
--- a/cpanfile
+++ b/cpanfile
@@ -3,16 +3,20 @@ requires 'CAM::PDF';
requires 'Crypt::Eksblowfish';
requires 'DateTime';
requires 'DateTime::Format::Strptime';
-requires 'Email::Sender';
-requires 'Geo::Distance';
-requires 'Geo::Distance::XS';
+requires 'Email::Sender::Simple';
+requires 'GIS::Distance';
+requires 'GIS::Distance::Fast';
requires 'List::UtilsBy';
+requires 'MIME::Entity';
requires 'Mojolicious';
requires 'Mojolicious::Plugin::Authentication';
+requires 'Mojolicious::Plugin::OAuth2';
requires 'Mojo::Pg';
-requires 'Travel::Status::DE::DBWagenreihung';
+requires 'Text::CSV';
+requires 'Text::Markdown';
+requires 'Travel::Status::DE::DBWagenreihung', '0.12';
+requires 'Travel::Status::DE::HAFAS', '>= 5.03';
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 2c7300b..2318de5 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -1,77 +1,97 @@
# carton snapshot format: version 1.0
DISTRIBUTIONS
- Alien-Build-1.74
- pathname: P/PL/PLICEASE/Alien-Build-1.74.tar.gz
- provides:
- Alien::Base 1.74
- Alien::Base::PkgConfig 1.74
- Alien::Base::Wrapper 1.74
- Alien::Build 1.74
- Alien::Build::CommandSequence 1.74
- Alien::Build::Interpolate 1.74
- Alien::Build::Interpolate::Default 1.74
- Alien::Build::Interpolate::Helper 1.74
- Alien::Build::MM 1.74
- Alien::Build::Meta 1.74
- Alien::Build::Plugin 1.74
- Alien::Build::Plugin::Build::Autoconf 1.74
- Alien::Build::Plugin::Build::CMake 1.74
- Alien::Build::Plugin::Build::MSYS 1.74
- Alien::Build::Plugin::Build::Make 1.74
- Alien::Build::Plugin::Build::SearchDep 1.74
- Alien::Build::Plugin::Core::CleanInstall 1.74
- Alien::Build::Plugin::Core::Download 1.74
- Alien::Build::Plugin::Core::FFI 1.74
- Alien::Build::Plugin::Core::Gather 1.74
- Alien::Build::Plugin::Core::Legacy 1.74
- Alien::Build::Plugin::Core::Override 1.74
- Alien::Build::Plugin::Core::Setup 1.74
- Alien::Build::Plugin::Core::Tail 1.74
- Alien::Build::Plugin::Decode::DirListing 1.74
- Alien::Build::Plugin::Decode::DirListingFtpcopy 1.74
- Alien::Build::Plugin::Decode::HTML 1.74
- Alien::Build::Plugin::Decode::Mojo 1.74
- Alien::Build::Plugin::Download::Negotiate 1.74
- Alien::Build::Plugin::Extract::ArchiveTar 1.74
- Alien::Build::Plugin::Extract::ArchiveZip 1.74
- Alien::Build::Plugin::Extract::CommandLine 1.74
- Alien::Build::Plugin::Extract::Directory 1.74
- Alien::Build::Plugin::Extract::Negotiate 1.74
- Alien::Build::Plugin::Fetch::CurlCommand 1.74
- Alien::Build::Plugin::Fetch::HTTPTiny 1.74
- Alien::Build::Plugin::Fetch::LWP 1.74
- Alien::Build::Plugin::Fetch::Local 1.74
- Alien::Build::Plugin::Fetch::LocalDir 1.74
- Alien::Build::Plugin::Fetch::NetFTP 1.74
- Alien::Build::Plugin::Fetch::Wget 1.74
- Alien::Build::Plugin::Gather::IsolateDynamic 1.74
- Alien::Build::Plugin::PkgConfig::CommandLine 1.74
- Alien::Build::Plugin::PkgConfig::LibPkgConf 1.74
- Alien::Build::Plugin::PkgConfig::MakeStatic 1.74
- Alien::Build::Plugin::PkgConfig::Negotiate 1.74
- Alien::Build::Plugin::PkgConfig::PP 1.74
- Alien::Build::Plugin::Prefer::BadVersion 1.74
- Alien::Build::Plugin::Prefer::GoodVersion 1.74
- Alien::Build::Plugin::Prefer::SortVersions 1.74
- Alien::Build::Plugin::Probe::CBuilder 1.74
- Alien::Build::Plugin::Probe::CommandLine 1.74
- Alien::Build::Plugin::Test::Mock 1.74
- Alien::Build::PluginMeta 1.74
- Alien::Build::TempDir 1.74
- Alien::Build::Util 1.74
- Alien::Build::Version::Basic 1.74
- Alien::Build::rc 1.74
- Alien::Role 1.74
- MY 1.74
- Test::Alien 1.74
- Test::Alien::Build 1.74
- Test::Alien::CanCompile 1.74
- Test::Alien::CanPlatypus 1.74
- Test::Alien::Run 1.74
- Test::Alien::Synthetic 1.74
- alienfile 1.74
+ Algorithm-Diff-1.201
+ pathname: R/RJ/RJBS/Algorithm-Diff-1.201.tar.gz
+ provides:
+ Algorithm::Diff 1.201
+ Algorithm::Diff::_impl 1.201
+ requirements:
+ ExtUtils::MakeMaker 0
+ Alien-Build-2.80
+ pathname: P/PL/PLICEASE/Alien-Build-2.80.tar.gz
+ provides:
+ Alien::Base 2.80
+ Alien::Base::PkgConfig 2.80
+ Alien::Base::Wrapper 2.80
+ Alien::Build 2.80
+ Alien::Build::CommandSequence 2.80
+ Alien::Build::Helper 2.80
+ Alien::Build::Interpolate 2.80
+ Alien::Build::Interpolate::Default 2.80
+ Alien::Build::Interpolate::Helper 2.80
+ Alien::Build::Log 2.80
+ Alien::Build::Log::Abbreviate 2.80
+ Alien::Build::Log::Default 2.80
+ Alien::Build::MM 2.80
+ Alien::Build::Meta 2.80
+ Alien::Build::Plugin 2.80
+ Alien::Build::Plugin::Build::Autoconf 2.80
+ Alien::Build::Plugin::Build::CMake 2.80
+ Alien::Build::Plugin::Build::Copy 2.80
+ Alien::Build::Plugin::Build::MSYS 2.80
+ Alien::Build::Plugin::Build::Make 2.80
+ Alien::Build::Plugin::Build::SearchDep 2.80
+ Alien::Build::Plugin::Core::CleanInstall 2.80
+ Alien::Build::Plugin::Core::Download 2.80
+ Alien::Build::Plugin::Core::FFI 2.80
+ Alien::Build::Plugin::Core::Gather 2.80
+ Alien::Build::Plugin::Core::Legacy 2.80
+ Alien::Build::Plugin::Core::Override 2.80
+ Alien::Build::Plugin::Core::Setup 2.80
+ Alien::Build::Plugin::Core::Tail 2.80
+ Alien::Build::Plugin::Decode::DirListing 2.80
+ Alien::Build::Plugin::Decode::DirListingFtpcopy 2.80
+ Alien::Build::Plugin::Decode::HTML 2.80
+ Alien::Build::Plugin::Decode::Mojo 2.80
+ Alien::Build::Plugin::Digest::Negotiate 2.80
+ Alien::Build::Plugin::Digest::SHA 2.80
+ Alien::Build::Plugin::Digest::SHAPP 2.80
+ Alien::Build::Plugin::Download::Negotiate 2.80
+ Alien::Build::Plugin::Extract::ArchiveTar 2.80
+ Alien::Build::Plugin::Extract::ArchiveZip 2.80
+ Alien::Build::Plugin::Extract::CommandLine 2.80
+ Alien::Build::Plugin::Extract::Directory 2.80
+ Alien::Build::Plugin::Extract::File 2.80
+ Alien::Build::Plugin::Extract::Negotiate 2.80
+ Alien::Build::Plugin::Fetch::CurlCommand 2.80
+ Alien::Build::Plugin::Fetch::HTTPTiny 2.80
+ Alien::Build::Plugin::Fetch::LWP 2.80
+ Alien::Build::Plugin::Fetch::Local 2.80
+ Alien::Build::Plugin::Fetch::LocalDir 2.80
+ Alien::Build::Plugin::Fetch::NetFTP 2.80
+ Alien::Build::Plugin::Fetch::Wget 2.80
+ Alien::Build::Plugin::Gather::IsolateDynamic 2.80
+ Alien::Build::Plugin::PkgConfig::CommandLine 2.80
+ Alien::Build::Plugin::PkgConfig::LibPkgConf 2.80
+ Alien::Build::Plugin::PkgConfig::MakeStatic 2.80
+ Alien::Build::Plugin::PkgConfig::Negotiate 2.80
+ Alien::Build::Plugin::PkgConfig::PP 2.80
+ Alien::Build::Plugin::Prefer::BadVersion 2.80
+ Alien::Build::Plugin::Prefer::GoodVersion 2.80
+ Alien::Build::Plugin::Prefer::SortVersions 2.80
+ Alien::Build::Plugin::Probe::CBuilder 2.80
+ Alien::Build::Plugin::Probe::CommandLine 2.80
+ Alien::Build::Plugin::Probe::Vcpkg 2.80
+ Alien::Build::Plugin::Test::Mock 2.80
+ Alien::Build::PluginMeta 2.80
+ Alien::Build::Temp 2.80
+ Alien::Build::TempDir 2.80
+ Alien::Build::Util 2.80
+ Alien::Build::Version::Basic 2.80
+ Alien::Build::rc 2.80
+ Alien::Role 2.80
+ Alien::Util 2.80
+ Test::Alien 2.80
+ Test::Alien::Build 2.80
+ Test::Alien::CanCompile 2.80
+ Test::Alien::CanPlatypus 2.80
+ Test::Alien::Diag 2.80
+ Test::Alien::Run 2.80
+ Test::Alien::Synthetic 2.80
+ alienfile 2.80
requirements:
Capture::Tiny 0.17
+ Digest::SHA 0
ExtUtils::CBuilder 0
ExtUtils::MakeMaker 6.64
ExtUtils::ParseXS 3.30
@@ -79,38 +99,46 @@ DISTRIBUTIONS
File::Which 1.10
File::chdir 0
JSON::PP 0
- Module::Load 0
+ List::Util 1.33
Path::Tiny 0.077
- Test2::API 1.302015
+ PkgConfig 0.14026
+ Test2::API 1.302096
Text::ParseWords 3.26
- perl 5.008001
- Alien-Libxml2-0.09
- pathname: P/PL/PLICEASE/Alien-Libxml2-0.09.tar.gz
+ parent 0
+ perl 5.008004
+ Alien-Build-Plugin-Download-GitLab-0.01
+ pathname: P/PL/PLICEASE/Alien-Build-Plugin-Download-GitLab-0.01.tar.gz
provides:
- Alien::Libxml2 0.09
+ Alien::Build::Plugin::Download::GitLab 0.01
requirements:
- Alien::Base 0.73
- Alien::Build 1.60
- Alien::Build::MM 1.60
+ Alien::Build::Plugin 0
+ ExtUtils::MakeMaker 0
+ JSON::PP 0
+ Path::Tiny 0
+ URI 0
+ URI::Escape 0
+ perl 5.008004
+ Alien-Libxml2-0.19
+ pathname: P/PL/PLICEASE/Alien-Libxml2-0.19.tar.gz
+ provides:
+ Alien::Libxml2 0.19
+ requirements:
+ Alien::Base 2.37
+ Alien::Build 2.37
+ Alien::Build::MM 2.37
Alien::Build::Plugin::Build::SearchDep 0.35
+ Alien::Build::Plugin::Download::GitLab 0
Alien::Build::Plugin::Prefer::BadVersion 1.05
- Archive::Tar 0
- Config 0
+ Alien::Build::Plugin::Probe::Vcpkg 0
ExtUtils::CBuilder 0
ExtUtils::MakeMaker 6.52
- HTML::LinkExtor 0
- HTTP::Tiny 0.044
- IO::Zlib 0
- Sort::Versions 0
- URI 0
- URI::Escape 0
perl 5.006
- B-Hooks-EndOfScope-0.24
- pathname: E/ET/ETHER/B-Hooks-EndOfScope-0.24.tar.gz
+ B-Hooks-EndOfScope-0.28
+ pathname: E/ET/ETHER/B-Hooks-EndOfScope-0.28.tar.gz
provides:
- B::Hooks::EndOfScope 0.24
- B::Hooks::EndOfScope::PP 0.24
- B::Hooks::EndOfScope::XS 0.24
+ B::Hooks::EndOfScope 0.28
+ B::Hooks::EndOfScope::PP 0.28
+ B::Hooks::EndOfScope::XS 0.28
requirements:
ExtUtils::MakeMaker 0
Hash::Util::FieldHash 0
@@ -135,6 +163,26 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
+ CAM-PDF-1.60
+ pathname: C/CD/CDOLAN/CAM-PDF-1.60.tar.gz
+ provides:
+ CAM::PDF 1.60
+ CAM::PDF::Content 1.60
+ CAM::PDF::Decrypt 1.60
+ CAM::PDF::GS 1.60
+ CAM::PDF::GS::NoText 1.60
+ CAM::PDF::Node 1.60
+ CAM::PDF::PageText 1.60
+ CAM::PDF::Renderer::Dump 1.60
+ CAM::PDF::Renderer::Images 1.60
+ CAM::PDF::Renderer::Text 1.60
+ CAM::PDF::Renderer::TextFB 1.60
+ requirements:
+ Crypt::RC4 2.02
+ Digest::MD5 2.16
+ Test::More 0
+ Text::PDF 0.29
+ perl v5.6.0
Cache-2.11
pathname: S/SH/SHLOMIF/Cache-2.11.tar.gz
provides:
@@ -202,35 +250,37 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
base 1.01
- Class-Data-Inheritable-0.08
- pathname: T/TM/TMTM/Class-Data-Inheritable-0.08.tar.gz
+ Class-Data-Inheritable-0.09
+ pathname: R/RS/RSHERER/Class-Data-Inheritable-0.09.tar.gz
provides:
- Class::Data::Inheritable 0.08
+ Class::Data::Inheritable 0.09
requirements:
ExtUtils::MakeMaker 0
- Class-Inspector-1.34
- pathname: P/PL/PLICEASE/Class-Inspector-1.34.tar.gz
+ Class-Inspector-1.36
+ pathname: P/PL/PLICEASE/Class-Inspector-1.36.tar.gz
provides:
- Class::Inspector 1.34
- Class::Inspector::Functions 1.34
+ Class::Inspector 1.36
+ Class::Inspector::Functions 1.36
requirements:
ExtUtils::MakeMaker 0
File::Spec 0.80
- perl 5.006
- Class-Measure-0.07
- pathname: B/BL/BLUEFEET/Class-Measure-0.07.tar.gz
+ base 0
+ perl 5.008
+ Class-Measure-0.10
+ pathname: B/BL/BLUEFEET/Class-Measure-0.10.tar.gz
provides:
- Class::Measure 0.07
- Class::Measure::Length 0.07
+ Class::Measure 0.10
+ Class::Measure::Length 0.10
requirements:
Carp 0
Module::Build::Tiny 0.035
+ Scalar::Util 0
Sub::Exporter 0.982
perl 5.008001
- Class-Method-Modifiers-2.12
- pathname: E/ET/ETHER/Class-Method-Modifiers-2.12.tar.gz
+ Class-Method-Modifiers-2.15
+ pathname: E/ET/ETHER/Class-Method-Modifiers-2.15.tar.gz
provides:
- Class::Method::Modifiers 2.12
+ Class::Method::Modifiers 2.15
requirements:
B 0
Carp 0
@@ -256,10 +306,19 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- Class-Singleton-1.5
- pathname: S/SH/SHAY/Class-Singleton-1.5.tar.gz
+ Class-Singleton-1.6
+ pathname: S/SH/SHAY/Class-Singleton-1.6.tar.gz
provides:
- Class::Singleton 1.5
+ Class::Singleton 1.6
+ requirements:
+ ExtUtils::MakeMaker 6.64
+ perl 5.008001
+ strict 0
+ warnings 0
+ Clone-0.46
+ pathname: G/GA/GARU/Clone-0.46.tar.gz
+ provides:
+ Clone 0.46
requirements:
ExtUtils::MakeMaker 0
Clone-Choose-0.010
@@ -305,19 +364,26 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- DBD-Pg-3.8.0
- pathname: T/TU/TURNSTEP/DBD-Pg-3.8.0.tar.gz
+ Crypt-RC4-2.02
+ pathname: S/SI/SIFUKURT/Crypt-RC4-2.02.tar.gz
+ provides:
+ Crypt::RC4 2.02
+ requirements:
+ ExtUtils::MakeMaker 0
+ DBD-Pg-3.18.0
+ pathname: T/TU/TURNSTEP/DBD-Pg-3.18.0.tar.gz
provides:
- Bundle::DBD::Pg v3.8.0
- DBD::Pg v3.8.0
+ Bundle::DBD::Pg v3.18.0
+ DBD::Pg v3.18.0
requirements:
DBI 1.614
- ExtUtils::MakeMaker 6.11
+ ExtUtils::MakeMaker 6.58
+ File::Temp 0
Test::More 0.88
Time::HiRes 0
version 0
- DBI-1.642
- pathname: T/TI/TIMB/DBI-1.642.tar.gz
+ DBI-1.643
+ pathname: T/TI/TIMB/DBI-1.643.tar.gz
provides:
Bundle::DBI 12.008696
DBD::DBM 0.08
@@ -373,7 +439,7 @@ DISTRIBUTIONS
DBD::Sponge::dr 12.010003
DBD::Sponge::st 12.010003
DBDI 12.015129
- DBI 1.642
+ DBI 1.643
DBI::Const::GetInfo::ANSI 2.008697
DBI::Const::GetInfo::ODBC 2.011374
DBI::Const::GetInfoReturn 2.008697
@@ -413,39 +479,40 @@ DISTRIBUTIONS
DBI::SQL::Nano::Table_ 1.015544
DBI::Util::CacheMemory 0.010315
DBI::Util::_accessor 0.009479
- DBI::common 1.642
+ DBI::common 1.643
requirements:
ExtUtils::MakeMaker 6.48
Test::Simple 0.90
- perl 5.008
- Data-OptList-0.110
- pathname: R/RJ/RJBS/Data-OptList-0.110.tar.gz
+ perl 5.008001
+ Data-OptList-0.114
+ pathname: R/RJ/RJBS/Data-OptList-0.114.tar.gz
provides:
- Data::OptList 0.110
+ Data::OptList 0.114
requirements:
- ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker 6.78
List::Util 0
Params::Util 0
Sub::Install 0.921
+ perl 5.012
strict 0
warnings 0
- DateTime-1.51
- pathname: D/DR/DROLSKY/DateTime-1.51.tar.gz
- provides:
- DateTime 1.51
- DateTime::Duration 1.51
- DateTime::Helpers 1.51
- DateTime::Infinite 1.51
- DateTime::Infinite::Future 1.51
- DateTime::Infinite::Past 1.51
- DateTime::LeapSecond 1.51
- DateTime::PP 1.51
- DateTime::PPExtra 1.51
- DateTime::Types 1.51
+ DateTime-1.65
+ pathname: D/DR/DROLSKY/DateTime-1.65.tar.gz
+ provides:
+ DateTime 1.65
+ DateTime::Duration 1.65
+ DateTime::Helpers 1.65
+ DateTime::Infinite 1.65
+ DateTime::Infinite::Future 1.65
+ DateTime::Infinite::Past 1.65
+ DateTime::LeapSecond 1.65
+ DateTime::PP 1.65
+ DateTime::PPExtra 1.65
+ DateTime::Types 1.65
requirements:
Carp 0
DateTime::Locale 1.06
- DateTime::TimeZone 2.02
+ DateTime::TimeZone 2.44
Dist::CheckConflicts 0.02
ExtUtils::MakeMaker 0
POSIX 0
@@ -457,9 +524,9 @@ DISTRIBUTIONS
Specio::Library::Builtins 0
Specio::Library::Numeric 0
Specio::Library::String 0
+ Specio::Subs 0
Try::Tiny 0
XSLoader 0
- base 0
integer 0
namespace::autoclean 0.19
overload 0
@@ -468,15 +535,15 @@ DISTRIBUTIONS
strict 0
warnings 0
warnings::register 0
- DateTime-Format-Strptime-1.76
- pathname: D/DR/DROLSKY/DateTime-Format-Strptime-1.76.tar.gz
+ DateTime-Format-Strptime-1.79
+ pathname: D/DR/DROLSKY/DateTime-Format-Strptime-1.79.tar.gz
provides:
- DateTime::Format::Strptime 1.76
- DateTime::Format::Strptime::Types 1.76
+ DateTime::Format::Strptime 1.79
+ DateTime::Format::Strptime::Types 1.79
requirements:
Carp 0
DateTime 1.00
- DateTime::Locale 1.23
+ DateTime::Locale 1.30
DateTime::Locale::Base 0
DateTime::Locale::FromData 0
DateTime::TimeZone 2.09
@@ -493,406 +560,372 @@ DISTRIBUTIONS
parent 0
strict 0
warnings 0
- DateTime-Locale-1.24
- pathname: D/DR/DROLSKY/DateTime-Locale-1.24.tar.gz
+ DateTime-Locale-1.41
+ pathname: D/DR/DROLSKY/DateTime-Locale-1.41.tar.gz
provides:
- DateTime::Locale 1.24
- DateTime::Locale::Base 1.24
- DateTime::Locale::Catalog 1.24
- DateTime::Locale::Data 1.24
- DateTime::Locale::FromData 1.24
- DateTime::Locale::Util 1.24
+ DateTime::Locale 1.41
+ DateTime::Locale::Base 1.41
+ DateTime::Locale::Catalog 1.41
+ DateTime::Locale::Data 1.41
+ DateTime::Locale::FromData 1.41
+ DateTime::Locale::Util 1.41
requirements:
Carp 0
Dist::CheckConflicts 0.02
Exporter 0
ExtUtils::MakeMaker 0
File::ShareDir 0
- File::ShareDir::Install 0.03
+ File::ShareDir::Install 0.06
+ File::Spec 0
List::Util 1.45
Params::ValidationCompiler 0.13
Specio::Declare 0
Specio::Library::String 0
+ Storable 0
namespace::autoclean 0.19
perl 5.008004
strict 0
warnings 0
- DateTime-TimeZone-2.35
- pathname: D/DR/DROLSKY/DateTime-TimeZone-2.35.tar.gz
- provides:
- DateTime::TimeZone 2.35
- DateTime::TimeZone::Africa::Abidjan 2.35
- DateTime::TimeZone::Africa::Accra 2.35
- DateTime::TimeZone::Africa::Algiers 2.35
- DateTime::TimeZone::Africa::Bissau 2.35
- DateTime::TimeZone::Africa::Cairo 2.35
- DateTime::TimeZone::Africa::Casablanca 2.35
- DateTime::TimeZone::Africa::Ceuta 2.35
- DateTime::TimeZone::Africa::El_Aaiun 2.35
- DateTime::TimeZone::Africa::Johannesburg 2.35
- DateTime::TimeZone::Africa::Juba 2.35
- DateTime::TimeZone::Africa::Khartoum 2.35
- DateTime::TimeZone::Africa::Lagos 2.35
- DateTime::TimeZone::Africa::Maputo 2.35
- DateTime::TimeZone::Africa::Monrovia 2.35
- DateTime::TimeZone::Africa::Nairobi 2.35
- DateTime::TimeZone::Africa::Ndjamena 2.35
- DateTime::TimeZone::Africa::Sao_Tome 2.35
- DateTime::TimeZone::Africa::Tripoli 2.35
- DateTime::TimeZone::Africa::Tunis 2.35
- DateTime::TimeZone::Africa::Windhoek 2.35
- DateTime::TimeZone::America::Adak 2.35
- DateTime::TimeZone::America::Anchorage 2.35
- DateTime::TimeZone::America::Araguaina 2.35
- DateTime::TimeZone::America::Argentina::Buenos_Aires 2.35
- DateTime::TimeZone::America::Argentina::Catamarca 2.35
- DateTime::TimeZone::America::Argentina::Cordoba 2.35
- DateTime::TimeZone::America::Argentina::Jujuy 2.35
- DateTime::TimeZone::America::Argentina::La_Rioja 2.35
- DateTime::TimeZone::America::Argentina::Mendoza 2.35
- DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.35
- DateTime::TimeZone::America::Argentina::Salta 2.35
- DateTime::TimeZone::America::Argentina::San_Juan 2.35
- DateTime::TimeZone::America::Argentina::San_Luis 2.35
- DateTime::TimeZone::America::Argentina::Tucuman 2.35
- DateTime::TimeZone::America::Argentina::Ushuaia 2.35
- DateTime::TimeZone::America::Asuncion 2.35
- DateTime::TimeZone::America::Atikokan 2.35
- DateTime::TimeZone::America::Bahia 2.35
- DateTime::TimeZone::America::Bahia_Banderas 2.35
- DateTime::TimeZone::America::Barbados 2.35
- DateTime::TimeZone::America::Belem 2.35
- DateTime::TimeZone::America::Belize 2.35
- DateTime::TimeZone::America::Blanc_Sablon 2.35
- DateTime::TimeZone::America::Boa_Vista 2.35
- DateTime::TimeZone::America::Bogota 2.35
- DateTime::TimeZone::America::Boise 2.35
- DateTime::TimeZone::America::Cambridge_Bay 2.35
- DateTime::TimeZone::America::Campo_Grande 2.35
- DateTime::TimeZone::America::Cancun 2.35
- DateTime::TimeZone::America::Caracas 2.35
- DateTime::TimeZone::America::Cayenne 2.35
- DateTime::TimeZone::America::Chicago 2.35
- DateTime::TimeZone::America::Chihuahua 2.35
- DateTime::TimeZone::America::Costa_Rica 2.35
- DateTime::TimeZone::America::Creston 2.35
- DateTime::TimeZone::America::Cuiaba 2.35
- DateTime::TimeZone::America::Curacao 2.35
- DateTime::TimeZone::America::Danmarkshavn 2.35
- DateTime::TimeZone::America::Dawson 2.35
- DateTime::TimeZone::America::Dawson_Creek 2.35
- DateTime::TimeZone::America::Denver 2.35
- DateTime::TimeZone::America::Detroit 2.35
- DateTime::TimeZone::America::Edmonton 2.35
- DateTime::TimeZone::America::Eirunepe 2.35
- DateTime::TimeZone::America::El_Salvador 2.35
- DateTime::TimeZone::America::Fort_Nelson 2.35
- DateTime::TimeZone::America::Fortaleza 2.35
- DateTime::TimeZone::America::Glace_Bay 2.35
- DateTime::TimeZone::America::Godthab 2.35
- DateTime::TimeZone::America::Goose_Bay 2.35
- DateTime::TimeZone::America::Grand_Turk 2.35
- DateTime::TimeZone::America::Guatemala 2.35
- DateTime::TimeZone::America::Guayaquil 2.35
- DateTime::TimeZone::America::Guyana 2.35
- DateTime::TimeZone::America::Halifax 2.35
- DateTime::TimeZone::America::Havana 2.35
- DateTime::TimeZone::America::Hermosillo 2.35
- DateTime::TimeZone::America::Indiana::Indianapolis 2.35
- DateTime::TimeZone::America::Indiana::Knox 2.35
- DateTime::TimeZone::America::Indiana::Marengo 2.35
- DateTime::TimeZone::America::Indiana::Petersburg 2.35
- DateTime::TimeZone::America::Indiana::Tell_City 2.35
- DateTime::TimeZone::America::Indiana::Vevay 2.35
- DateTime::TimeZone::America::Indiana::Vincennes 2.35
- DateTime::TimeZone::America::Indiana::Winamac 2.35
- DateTime::TimeZone::America::Inuvik 2.35
- DateTime::TimeZone::America::Iqaluit 2.35
- DateTime::TimeZone::America::Jamaica 2.35
- DateTime::TimeZone::America::Juneau 2.35
- DateTime::TimeZone::America::Kentucky::Louisville 2.35
- DateTime::TimeZone::America::Kentucky::Monticello 2.35
- DateTime::TimeZone::America::La_Paz 2.35
- DateTime::TimeZone::America::Lima 2.35
- DateTime::TimeZone::America::Los_Angeles 2.35
- DateTime::TimeZone::America::Maceio 2.35
- DateTime::TimeZone::America::Managua 2.35
- DateTime::TimeZone::America::Manaus 2.35
- DateTime::TimeZone::America::Martinique 2.35
- DateTime::TimeZone::America::Matamoros 2.35
- DateTime::TimeZone::America::Mazatlan 2.35
- DateTime::TimeZone::America::Menominee 2.35
- DateTime::TimeZone::America::Merida 2.35
- DateTime::TimeZone::America::Metlakatla 2.35
- DateTime::TimeZone::America::Mexico_City 2.35
- DateTime::TimeZone::America::Miquelon 2.35
- DateTime::TimeZone::America::Moncton 2.35
- DateTime::TimeZone::America::Monterrey 2.35
- DateTime::TimeZone::America::Montevideo 2.35
- DateTime::TimeZone::America::Nassau 2.35
- DateTime::TimeZone::America::New_York 2.35
- DateTime::TimeZone::America::Nipigon 2.35
- DateTime::TimeZone::America::Nome 2.35
- DateTime::TimeZone::America::Noronha 2.35
- DateTime::TimeZone::America::North_Dakota::Beulah 2.35
- DateTime::TimeZone::America::North_Dakota::Center 2.35
- DateTime::TimeZone::America::North_Dakota::New_Salem 2.35
- DateTime::TimeZone::America::Ojinaga 2.35
- DateTime::TimeZone::America::Panama 2.35
- DateTime::TimeZone::America::Pangnirtung 2.35
- DateTime::TimeZone::America::Paramaribo 2.35
- DateTime::TimeZone::America::Phoenix 2.35
- DateTime::TimeZone::America::Port_au_Prince 2.35
- DateTime::TimeZone::America::Port_of_Spain 2.35
- DateTime::TimeZone::America::Porto_Velho 2.35
- DateTime::TimeZone::America::Puerto_Rico 2.35
- DateTime::TimeZone::America::Punta_Arenas 2.35
- DateTime::TimeZone::America::Rainy_River 2.35
- DateTime::TimeZone::America::Rankin_Inlet 2.35
- DateTime::TimeZone::America::Recife 2.35
- DateTime::TimeZone::America::Regina 2.35
- DateTime::TimeZone::America::Resolute 2.35
- DateTime::TimeZone::America::Rio_Branco 2.35
- DateTime::TimeZone::America::Santarem 2.35
- DateTime::TimeZone::America::Santiago 2.35
- DateTime::TimeZone::America::Santo_Domingo 2.35
- DateTime::TimeZone::America::Sao_Paulo 2.35
- DateTime::TimeZone::America::Scoresbysund 2.35
- DateTime::TimeZone::America::Sitka 2.35
- DateTime::TimeZone::America::St_Johns 2.35
- DateTime::TimeZone::America::Swift_Current 2.35
- DateTime::TimeZone::America::Tegucigalpa 2.35
- DateTime::TimeZone::America::Thule 2.35
- DateTime::TimeZone::America::Thunder_Bay 2.35
- DateTime::TimeZone::America::Tijuana 2.35
- DateTime::TimeZone::America::Toronto 2.35
- DateTime::TimeZone::America::Vancouver 2.35
- DateTime::TimeZone::America::Whitehorse 2.35
- DateTime::TimeZone::America::Winnipeg 2.35
- DateTime::TimeZone::America::Yakutat 2.35
- DateTime::TimeZone::America::Yellowknife 2.35
- DateTime::TimeZone::Antarctica::Casey 2.35
- DateTime::TimeZone::Antarctica::Davis 2.35
- DateTime::TimeZone::Antarctica::DumontDUrville 2.35
- DateTime::TimeZone::Antarctica::Macquarie 2.35
- DateTime::TimeZone::Antarctica::Mawson 2.35
- DateTime::TimeZone::Antarctica::Palmer 2.35
- DateTime::TimeZone::Antarctica::Rothera 2.35
- DateTime::TimeZone::Antarctica::Syowa 2.35
- DateTime::TimeZone::Antarctica::Troll 2.35
- DateTime::TimeZone::Antarctica::Vostok 2.35
- DateTime::TimeZone::Asia::Almaty 2.35
- DateTime::TimeZone::Asia::Amman 2.35
- DateTime::TimeZone::Asia::Anadyr 2.35
- DateTime::TimeZone::Asia::Aqtau 2.35
- DateTime::TimeZone::Asia::Aqtobe 2.35
- DateTime::TimeZone::Asia::Ashgabat 2.35
- DateTime::TimeZone::Asia::Atyrau 2.35
- DateTime::TimeZone::Asia::Baghdad 2.35
- DateTime::TimeZone::Asia::Baku 2.35
- DateTime::TimeZone::Asia::Bangkok 2.35
- DateTime::TimeZone::Asia::Barnaul 2.35
- DateTime::TimeZone::Asia::Beirut 2.35
- DateTime::TimeZone::Asia::Bishkek 2.35
- DateTime::TimeZone::Asia::Brunei 2.35
- DateTime::TimeZone::Asia::Chita 2.35
- DateTime::TimeZone::Asia::Choibalsan 2.35
- DateTime::TimeZone::Asia::Colombo 2.35
- DateTime::TimeZone::Asia::Damascus 2.35
- DateTime::TimeZone::Asia::Dhaka 2.35
- DateTime::TimeZone::Asia::Dili 2.35
- DateTime::TimeZone::Asia::Dubai 2.35
- DateTime::TimeZone::Asia::Dushanbe 2.35
- DateTime::TimeZone::Asia::Famagusta 2.35
- DateTime::TimeZone::Asia::Gaza 2.35
- DateTime::TimeZone::Asia::Hebron 2.35
- DateTime::TimeZone::Asia::Ho_Chi_Minh 2.35
- DateTime::TimeZone::Asia::Hong_Kong 2.35
- DateTime::TimeZone::Asia::Hovd 2.35
- DateTime::TimeZone::Asia::Irkutsk 2.35
- DateTime::TimeZone::Asia::Jakarta 2.35
- DateTime::TimeZone::Asia::Jayapura 2.35
- DateTime::TimeZone::Asia::Jerusalem 2.35
- DateTime::TimeZone::Asia::Kabul 2.35
- DateTime::TimeZone::Asia::Kamchatka 2.35
- DateTime::TimeZone::Asia::Karachi 2.35
- DateTime::TimeZone::Asia::Kathmandu 2.35
- DateTime::TimeZone::Asia::Khandyga 2.35
- DateTime::TimeZone::Asia::Kolkata 2.35
- DateTime::TimeZone::Asia::Krasnoyarsk 2.35
- DateTime::TimeZone::Asia::Kuala_Lumpur 2.35
- DateTime::TimeZone::Asia::Kuching 2.35
- DateTime::TimeZone::Asia::Macau 2.35
- DateTime::TimeZone::Asia::Magadan 2.35
- DateTime::TimeZone::Asia::Makassar 2.35
- DateTime::TimeZone::Asia::Manila 2.35
- DateTime::TimeZone::Asia::Nicosia 2.35
- DateTime::TimeZone::Asia::Novokuznetsk 2.35
- DateTime::TimeZone::Asia::Novosibirsk 2.35
- DateTime::TimeZone::Asia::Omsk 2.35
- DateTime::TimeZone::Asia::Oral 2.35
- DateTime::TimeZone::Asia::Pontianak 2.35
- DateTime::TimeZone::Asia::Pyongyang 2.35
- DateTime::TimeZone::Asia::Qatar 2.35
- DateTime::TimeZone::Asia::Qostanay 2.35
- DateTime::TimeZone::Asia::Qyzylorda 2.35
- DateTime::TimeZone::Asia::Riyadh 2.35
- DateTime::TimeZone::Asia::Sakhalin 2.35
- DateTime::TimeZone::Asia::Samarkand 2.35
- DateTime::TimeZone::Asia::Seoul 2.35
- DateTime::TimeZone::Asia::Shanghai 2.35
- DateTime::TimeZone::Asia::Singapore 2.35
- DateTime::TimeZone::Asia::Srednekolymsk 2.35
- DateTime::TimeZone::Asia::Taipei 2.35
- DateTime::TimeZone::Asia::Tashkent 2.35
- DateTime::TimeZone::Asia::Tbilisi 2.35
- DateTime::TimeZone::Asia::Tehran 2.35
- DateTime::TimeZone::Asia::Thimphu 2.35
- DateTime::TimeZone::Asia::Tokyo 2.35
- DateTime::TimeZone::Asia::Tomsk 2.35
- DateTime::TimeZone::Asia::Ulaanbaatar 2.35
- DateTime::TimeZone::Asia::Urumqi 2.35
- DateTime::TimeZone::Asia::Ust_Nera 2.35
- DateTime::TimeZone::Asia::Vladivostok 2.35
- DateTime::TimeZone::Asia::Yakutsk 2.35
- DateTime::TimeZone::Asia::Yangon 2.35
- DateTime::TimeZone::Asia::Yekaterinburg 2.35
- DateTime::TimeZone::Asia::Yerevan 2.35
- DateTime::TimeZone::Atlantic::Azores 2.35
- DateTime::TimeZone::Atlantic::Bermuda 2.35
- DateTime::TimeZone::Atlantic::Canary 2.35
- DateTime::TimeZone::Atlantic::Cape_Verde 2.35
- DateTime::TimeZone::Atlantic::Faroe 2.35
- DateTime::TimeZone::Atlantic::Madeira 2.35
- DateTime::TimeZone::Atlantic::Reykjavik 2.35
- DateTime::TimeZone::Atlantic::South_Georgia 2.35
- DateTime::TimeZone::Atlantic::Stanley 2.35
- DateTime::TimeZone::Australia::Adelaide 2.35
- DateTime::TimeZone::Australia::Brisbane 2.35
- DateTime::TimeZone::Australia::Broken_Hill 2.35
- DateTime::TimeZone::Australia::Currie 2.35
- DateTime::TimeZone::Australia::Darwin 2.35
- DateTime::TimeZone::Australia::Eucla 2.35
- DateTime::TimeZone::Australia::Hobart 2.35
- DateTime::TimeZone::Australia::Lindeman 2.35
- DateTime::TimeZone::Australia::Lord_Howe 2.35
- DateTime::TimeZone::Australia::Melbourne 2.35
- DateTime::TimeZone::Australia::Perth 2.35
- DateTime::TimeZone::Australia::Sydney 2.35
- DateTime::TimeZone::CET 2.35
- DateTime::TimeZone::CST6CDT 2.35
- DateTime::TimeZone::Catalog 2.35
- DateTime::TimeZone::EET 2.35
- DateTime::TimeZone::EST 2.35
- DateTime::TimeZone::EST5EDT 2.35
- DateTime::TimeZone::Europe::Amsterdam 2.35
- DateTime::TimeZone::Europe::Andorra 2.35
- DateTime::TimeZone::Europe::Astrakhan 2.35
- DateTime::TimeZone::Europe::Athens 2.35
- DateTime::TimeZone::Europe::Belgrade 2.35
- DateTime::TimeZone::Europe::Berlin 2.35
- DateTime::TimeZone::Europe::Brussels 2.35
- DateTime::TimeZone::Europe::Bucharest 2.35
- DateTime::TimeZone::Europe::Budapest 2.35
- DateTime::TimeZone::Europe::Chisinau 2.35
- DateTime::TimeZone::Europe::Copenhagen 2.35
- DateTime::TimeZone::Europe::Dublin 2.35
- DateTime::TimeZone::Europe::Gibraltar 2.35
- DateTime::TimeZone::Europe::Helsinki 2.35
- DateTime::TimeZone::Europe::Istanbul 2.35
- DateTime::TimeZone::Europe::Kaliningrad 2.35
- DateTime::TimeZone::Europe::Kiev 2.35
- DateTime::TimeZone::Europe::Kirov 2.35
- DateTime::TimeZone::Europe::Lisbon 2.35
- DateTime::TimeZone::Europe::London 2.35
- DateTime::TimeZone::Europe::Luxembourg 2.35
- DateTime::TimeZone::Europe::Madrid 2.35
- DateTime::TimeZone::Europe::Malta 2.35
- DateTime::TimeZone::Europe::Minsk 2.35
- DateTime::TimeZone::Europe::Monaco 2.35
- DateTime::TimeZone::Europe::Moscow 2.35
- DateTime::TimeZone::Europe::Oslo 2.35
- DateTime::TimeZone::Europe::Paris 2.35
- DateTime::TimeZone::Europe::Prague 2.35
- DateTime::TimeZone::Europe::Riga 2.35
- DateTime::TimeZone::Europe::Rome 2.35
- DateTime::TimeZone::Europe::Samara 2.35
- DateTime::TimeZone::Europe::Saratov 2.35
- DateTime::TimeZone::Europe::Simferopol 2.35
- DateTime::TimeZone::Europe::Sofia 2.35
- DateTime::TimeZone::Europe::Stockholm 2.35
- DateTime::TimeZone::Europe::Tallinn 2.35
- DateTime::TimeZone::Europe::Tirane 2.35
- DateTime::TimeZone::Europe::Ulyanovsk 2.35
- DateTime::TimeZone::Europe::Uzhgorod 2.35
- DateTime::TimeZone::Europe::Vienna 2.35
- DateTime::TimeZone::Europe::Vilnius 2.35
- DateTime::TimeZone::Europe::Volgograd 2.35
- DateTime::TimeZone::Europe::Warsaw 2.35
- DateTime::TimeZone::Europe::Zaporozhye 2.35
- DateTime::TimeZone::Europe::Zurich 2.35
- DateTime::TimeZone::Floating 2.35
- DateTime::TimeZone::HST 2.35
- DateTime::TimeZone::Indian::Chagos 2.35
- DateTime::TimeZone::Indian::Christmas 2.35
- DateTime::TimeZone::Indian::Cocos 2.35
- DateTime::TimeZone::Indian::Kerguelen 2.35
- DateTime::TimeZone::Indian::Mahe 2.35
- DateTime::TimeZone::Indian::Maldives 2.35
- DateTime::TimeZone::Indian::Mauritius 2.35
- DateTime::TimeZone::Indian::Reunion 2.35
- DateTime::TimeZone::Local 2.35
- DateTime::TimeZone::Local::Android 2.35
- DateTime::TimeZone::Local::Unix 2.35
- DateTime::TimeZone::Local::VMS 2.35
- DateTime::TimeZone::MET 2.35
- DateTime::TimeZone::MST 2.35
- DateTime::TimeZone::MST7MDT 2.35
- DateTime::TimeZone::OffsetOnly 2.35
- DateTime::TimeZone::OlsonDB 2.35
- DateTime::TimeZone::OlsonDB::Change 2.35
- DateTime::TimeZone::OlsonDB::Observance 2.35
- DateTime::TimeZone::OlsonDB::Rule 2.35
- DateTime::TimeZone::OlsonDB::Zone 2.35
- DateTime::TimeZone::PST8PDT 2.35
- DateTime::TimeZone::Pacific::Apia 2.35
- DateTime::TimeZone::Pacific::Auckland 2.35
- DateTime::TimeZone::Pacific::Bougainville 2.35
- DateTime::TimeZone::Pacific::Chatham 2.35
- DateTime::TimeZone::Pacific::Chuuk 2.35
- DateTime::TimeZone::Pacific::Easter 2.35
- DateTime::TimeZone::Pacific::Efate 2.35
- DateTime::TimeZone::Pacific::Enderbury 2.35
- DateTime::TimeZone::Pacific::Fakaofo 2.35
- DateTime::TimeZone::Pacific::Fiji 2.35
- DateTime::TimeZone::Pacific::Funafuti 2.35
- DateTime::TimeZone::Pacific::Galapagos 2.35
- DateTime::TimeZone::Pacific::Gambier 2.35
- DateTime::TimeZone::Pacific::Guadalcanal 2.35
- DateTime::TimeZone::Pacific::Guam 2.35
- DateTime::TimeZone::Pacific::Honolulu 2.35
- DateTime::TimeZone::Pacific::Kiritimati 2.35
- DateTime::TimeZone::Pacific::Kosrae 2.35
- DateTime::TimeZone::Pacific::Kwajalein 2.35
- DateTime::TimeZone::Pacific::Majuro 2.35
- DateTime::TimeZone::Pacific::Marquesas 2.35
- DateTime::TimeZone::Pacific::Nauru 2.35
- DateTime::TimeZone::Pacific::Niue 2.35
- DateTime::TimeZone::Pacific::Norfolk 2.35
- DateTime::TimeZone::Pacific::Noumea 2.35
- DateTime::TimeZone::Pacific::Pago_Pago 2.35
- DateTime::TimeZone::Pacific::Palau 2.35
- DateTime::TimeZone::Pacific::Pitcairn 2.35
- DateTime::TimeZone::Pacific::Pohnpei 2.35
- DateTime::TimeZone::Pacific::Port_Moresby 2.35
- DateTime::TimeZone::Pacific::Rarotonga 2.35
- DateTime::TimeZone::Pacific::Tahiti 2.35
- DateTime::TimeZone::Pacific::Tarawa 2.35
- DateTime::TimeZone::Pacific::Tongatapu 2.35
- DateTime::TimeZone::Pacific::Wake 2.35
- DateTime::TimeZone::Pacific::Wallis 2.35
- DateTime::TimeZone::UTC 2.35
- DateTime::TimeZone::WET 2.35
+ DateTime-TimeZone-2.62
+ pathname: D/DR/DROLSKY/DateTime-TimeZone-2.62.tar.gz
+ provides:
+ DateTime::TimeZone 2.62
+ DateTime::TimeZone::Africa::Abidjan 2.62
+ DateTime::TimeZone::Africa::Algiers 2.62
+ DateTime::TimeZone::Africa::Bissau 2.62
+ DateTime::TimeZone::Africa::Cairo 2.62
+ DateTime::TimeZone::Africa::Casablanca 2.62
+ DateTime::TimeZone::Africa::Ceuta 2.62
+ DateTime::TimeZone::Africa::El_Aaiun 2.62
+ DateTime::TimeZone::Africa::Johannesburg 2.62
+ DateTime::TimeZone::Africa::Juba 2.62
+ DateTime::TimeZone::Africa::Khartoum 2.62
+ DateTime::TimeZone::Africa::Lagos 2.62
+ DateTime::TimeZone::Africa::Maputo 2.62
+ DateTime::TimeZone::Africa::Monrovia 2.62
+ DateTime::TimeZone::Africa::Nairobi 2.62
+ DateTime::TimeZone::Africa::Ndjamena 2.62
+ DateTime::TimeZone::Africa::Sao_Tome 2.62
+ DateTime::TimeZone::Africa::Tripoli 2.62
+ DateTime::TimeZone::Africa::Tunis 2.62
+ DateTime::TimeZone::Africa::Windhoek 2.62
+ DateTime::TimeZone::America::Adak 2.62
+ DateTime::TimeZone::America::Anchorage 2.62
+ DateTime::TimeZone::America::Araguaina 2.62
+ DateTime::TimeZone::America::Argentina::Buenos_Aires 2.62
+ DateTime::TimeZone::America::Argentina::Catamarca 2.62
+ DateTime::TimeZone::America::Argentina::Cordoba 2.62
+ DateTime::TimeZone::America::Argentina::Jujuy 2.62
+ DateTime::TimeZone::America::Argentina::La_Rioja 2.62
+ DateTime::TimeZone::America::Argentina::Mendoza 2.62
+ DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.62
+ DateTime::TimeZone::America::Argentina::Salta 2.62
+ DateTime::TimeZone::America::Argentina::San_Juan 2.62
+ DateTime::TimeZone::America::Argentina::San_Luis 2.62
+ DateTime::TimeZone::America::Argentina::Tucuman 2.62
+ DateTime::TimeZone::America::Argentina::Ushuaia 2.62
+ DateTime::TimeZone::America::Asuncion 2.62
+ DateTime::TimeZone::America::Bahia 2.62
+ DateTime::TimeZone::America::Bahia_Banderas 2.62
+ DateTime::TimeZone::America::Barbados 2.62
+ DateTime::TimeZone::America::Belem 2.62
+ DateTime::TimeZone::America::Belize 2.62
+ DateTime::TimeZone::America::Boa_Vista 2.62
+ DateTime::TimeZone::America::Bogota 2.62
+ DateTime::TimeZone::America::Boise 2.62
+ DateTime::TimeZone::America::Cambridge_Bay 2.62
+ DateTime::TimeZone::America::Campo_Grande 2.62
+ DateTime::TimeZone::America::Cancun 2.62
+ DateTime::TimeZone::America::Caracas 2.62
+ DateTime::TimeZone::America::Cayenne 2.62
+ DateTime::TimeZone::America::Chicago 2.62
+ DateTime::TimeZone::America::Chihuahua 2.62
+ DateTime::TimeZone::America::Ciudad_Juarez 2.62
+ DateTime::TimeZone::America::Costa_Rica 2.62
+ DateTime::TimeZone::America::Cuiaba 2.62
+ DateTime::TimeZone::America::Danmarkshavn 2.62
+ DateTime::TimeZone::America::Dawson 2.62
+ DateTime::TimeZone::America::Dawson_Creek 2.62
+ DateTime::TimeZone::America::Denver 2.62
+ DateTime::TimeZone::America::Detroit 2.62
+ DateTime::TimeZone::America::Edmonton 2.62
+ DateTime::TimeZone::America::Eirunepe 2.62
+ DateTime::TimeZone::America::El_Salvador 2.62
+ DateTime::TimeZone::America::Fort_Nelson 2.62
+ DateTime::TimeZone::America::Fortaleza 2.62
+ DateTime::TimeZone::America::Glace_Bay 2.62
+ DateTime::TimeZone::America::Goose_Bay 2.62
+ DateTime::TimeZone::America::Grand_Turk 2.62
+ DateTime::TimeZone::America::Guatemala 2.62
+ DateTime::TimeZone::America::Guayaquil 2.62
+ DateTime::TimeZone::America::Guyana 2.62
+ DateTime::TimeZone::America::Halifax 2.62
+ DateTime::TimeZone::America::Havana 2.62
+ DateTime::TimeZone::America::Hermosillo 2.62
+ DateTime::TimeZone::America::Indiana::Indianapolis 2.62
+ DateTime::TimeZone::America::Indiana::Knox 2.62
+ DateTime::TimeZone::America::Indiana::Marengo 2.62
+ DateTime::TimeZone::America::Indiana::Petersburg 2.62
+ DateTime::TimeZone::America::Indiana::Tell_City 2.62
+ DateTime::TimeZone::America::Indiana::Vevay 2.62
+ DateTime::TimeZone::America::Indiana::Vincennes 2.62
+ DateTime::TimeZone::America::Indiana::Winamac 2.62
+ DateTime::TimeZone::America::Inuvik 2.62
+ DateTime::TimeZone::America::Iqaluit 2.62
+ DateTime::TimeZone::America::Jamaica 2.62
+ DateTime::TimeZone::America::Juneau 2.62
+ DateTime::TimeZone::America::Kentucky::Louisville 2.62
+ DateTime::TimeZone::America::Kentucky::Monticello 2.62
+ DateTime::TimeZone::America::La_Paz 2.62
+ DateTime::TimeZone::America::Lima 2.62
+ DateTime::TimeZone::America::Los_Angeles 2.62
+ DateTime::TimeZone::America::Maceio 2.62
+ DateTime::TimeZone::America::Managua 2.62
+ DateTime::TimeZone::America::Manaus 2.62
+ DateTime::TimeZone::America::Martinique 2.62
+ DateTime::TimeZone::America::Matamoros 2.62
+ DateTime::TimeZone::America::Mazatlan 2.62
+ DateTime::TimeZone::America::Menominee 2.62
+ DateTime::TimeZone::America::Merida 2.62
+ DateTime::TimeZone::America::Metlakatla 2.62
+ DateTime::TimeZone::America::Mexico_City 2.62
+ DateTime::TimeZone::America::Miquelon 2.62
+ DateTime::TimeZone::America::Moncton 2.62
+ DateTime::TimeZone::America::Monterrey 2.62
+ DateTime::TimeZone::America::Montevideo 2.62
+ DateTime::TimeZone::America::New_York 2.62
+ DateTime::TimeZone::America::Nome 2.62
+ DateTime::TimeZone::America::Noronha 2.62
+ DateTime::TimeZone::America::North_Dakota::Beulah 2.62
+ DateTime::TimeZone::America::North_Dakota::Center 2.62
+ DateTime::TimeZone::America::North_Dakota::New_Salem 2.62
+ DateTime::TimeZone::America::Nuuk 2.62
+ DateTime::TimeZone::America::Ojinaga 2.62
+ DateTime::TimeZone::America::Panama 2.62
+ DateTime::TimeZone::America::Paramaribo 2.62
+ DateTime::TimeZone::America::Phoenix 2.62
+ DateTime::TimeZone::America::Port_au_Prince 2.62
+ DateTime::TimeZone::America::Porto_Velho 2.62
+ DateTime::TimeZone::America::Puerto_Rico 2.62
+ DateTime::TimeZone::America::Punta_Arenas 2.62
+ DateTime::TimeZone::America::Rankin_Inlet 2.62
+ DateTime::TimeZone::America::Recife 2.62
+ DateTime::TimeZone::America::Regina 2.62
+ DateTime::TimeZone::America::Resolute 2.62
+ DateTime::TimeZone::America::Rio_Branco 2.62
+ DateTime::TimeZone::America::Santarem 2.62
+ DateTime::TimeZone::America::Santiago 2.62
+ DateTime::TimeZone::America::Santo_Domingo 2.62
+ DateTime::TimeZone::America::Sao_Paulo 2.62
+ DateTime::TimeZone::America::Scoresbysund 2.62
+ DateTime::TimeZone::America::Sitka 2.62
+ DateTime::TimeZone::America::St_Johns 2.62
+ DateTime::TimeZone::America::Swift_Current 2.62
+ DateTime::TimeZone::America::Tegucigalpa 2.62
+ DateTime::TimeZone::America::Thule 2.62
+ DateTime::TimeZone::America::Tijuana 2.62
+ DateTime::TimeZone::America::Toronto 2.62
+ DateTime::TimeZone::America::Vancouver 2.62
+ DateTime::TimeZone::America::Whitehorse 2.62
+ DateTime::TimeZone::America::Winnipeg 2.62
+ DateTime::TimeZone::America::Yakutat 2.62
+ DateTime::TimeZone::Antarctica::Casey 2.62
+ DateTime::TimeZone::Antarctica::Davis 2.62
+ DateTime::TimeZone::Antarctica::Macquarie 2.62
+ DateTime::TimeZone::Antarctica::Mawson 2.62
+ DateTime::TimeZone::Antarctica::Palmer 2.62
+ DateTime::TimeZone::Antarctica::Rothera 2.62
+ DateTime::TimeZone::Antarctica::Troll 2.62
+ DateTime::TimeZone::Antarctica::Vostok 2.62
+ DateTime::TimeZone::Asia::Almaty 2.62
+ DateTime::TimeZone::Asia::Amman 2.62
+ DateTime::TimeZone::Asia::Anadyr 2.62
+ DateTime::TimeZone::Asia::Aqtau 2.62
+ DateTime::TimeZone::Asia::Aqtobe 2.62
+ DateTime::TimeZone::Asia::Ashgabat 2.62
+ DateTime::TimeZone::Asia::Atyrau 2.62
+ DateTime::TimeZone::Asia::Baghdad 2.62
+ DateTime::TimeZone::Asia::Baku 2.62
+ DateTime::TimeZone::Asia::Bangkok 2.62
+ DateTime::TimeZone::Asia::Barnaul 2.62
+ DateTime::TimeZone::Asia::Beirut 2.62
+ DateTime::TimeZone::Asia::Bishkek 2.62
+ DateTime::TimeZone::Asia::Chita 2.62
+ DateTime::TimeZone::Asia::Choibalsan 2.62
+ DateTime::TimeZone::Asia::Colombo 2.62
+ DateTime::TimeZone::Asia::Damascus 2.62
+ DateTime::TimeZone::Asia::Dhaka 2.62
+ DateTime::TimeZone::Asia::Dili 2.62
+ DateTime::TimeZone::Asia::Dubai 2.62
+ DateTime::TimeZone::Asia::Dushanbe 2.62
+ DateTime::TimeZone::Asia::Famagusta 2.62
+ DateTime::TimeZone::Asia::Gaza 2.62
+ DateTime::TimeZone::Asia::Hebron 2.62
+ DateTime::TimeZone::Asia::Ho_Chi_Minh 2.62
+ DateTime::TimeZone::Asia::Hong_Kong 2.62
+ DateTime::TimeZone::Asia::Hovd 2.62
+ DateTime::TimeZone::Asia::Irkutsk 2.62
+ DateTime::TimeZone::Asia::Jakarta 2.62
+ DateTime::TimeZone::Asia::Jayapura 2.62
+ DateTime::TimeZone::Asia::Jerusalem 2.62
+ DateTime::TimeZone::Asia::Kabul 2.62
+ DateTime::TimeZone::Asia::Kamchatka 2.62
+ DateTime::TimeZone::Asia::Karachi 2.62
+ DateTime::TimeZone::Asia::Kathmandu 2.62
+ DateTime::TimeZone::Asia::Khandyga 2.62
+ DateTime::TimeZone::Asia::Kolkata 2.62
+ DateTime::TimeZone::Asia::Krasnoyarsk 2.62
+ DateTime::TimeZone::Asia::Kuching 2.62
+ DateTime::TimeZone::Asia::Macau 2.62
+ DateTime::TimeZone::Asia::Magadan 2.62
+ DateTime::TimeZone::Asia::Makassar 2.62
+ DateTime::TimeZone::Asia::Manila 2.62
+ DateTime::TimeZone::Asia::Nicosia 2.62
+ DateTime::TimeZone::Asia::Novokuznetsk 2.62
+ DateTime::TimeZone::Asia::Novosibirsk 2.62
+ DateTime::TimeZone::Asia::Omsk 2.62
+ DateTime::TimeZone::Asia::Oral 2.62
+ DateTime::TimeZone::Asia::Pontianak 2.62
+ DateTime::TimeZone::Asia::Pyongyang 2.62
+ DateTime::TimeZone::Asia::Qatar 2.62
+ DateTime::TimeZone::Asia::Qostanay 2.62
+ DateTime::TimeZone::Asia::Qyzylorda 2.62
+ DateTime::TimeZone::Asia::Riyadh 2.62
+ DateTime::TimeZone::Asia::Sakhalin 2.62
+ DateTime::TimeZone::Asia::Samarkand 2.62
+ DateTime::TimeZone::Asia::Seoul 2.62
+ DateTime::TimeZone::Asia::Shanghai 2.62
+ DateTime::TimeZone::Asia::Singapore 2.62
+ DateTime::TimeZone::Asia::Srednekolymsk 2.62
+ DateTime::TimeZone::Asia::Taipei 2.62
+ DateTime::TimeZone::Asia::Tashkent 2.62
+ DateTime::TimeZone::Asia::Tbilisi 2.62
+ DateTime::TimeZone::Asia::Tehran 2.62
+ DateTime::TimeZone::Asia::Thimphu 2.62
+ DateTime::TimeZone::Asia::Tokyo 2.62
+ DateTime::TimeZone::Asia::Tomsk 2.62
+ DateTime::TimeZone::Asia::Ulaanbaatar 2.62
+ DateTime::TimeZone::Asia::Urumqi 2.62
+ DateTime::TimeZone::Asia::Ust_Nera 2.62
+ DateTime::TimeZone::Asia::Vladivostok 2.62
+ DateTime::TimeZone::Asia::Yakutsk 2.62
+ DateTime::TimeZone::Asia::Yangon 2.62
+ DateTime::TimeZone::Asia::Yekaterinburg 2.62
+ DateTime::TimeZone::Asia::Yerevan 2.62
+ DateTime::TimeZone::Atlantic::Azores 2.62
+ DateTime::TimeZone::Atlantic::Bermuda 2.62
+ DateTime::TimeZone::Atlantic::Canary 2.62
+ DateTime::TimeZone::Atlantic::Cape_Verde 2.62
+ DateTime::TimeZone::Atlantic::Faroe 2.62
+ DateTime::TimeZone::Atlantic::Madeira 2.62
+ DateTime::TimeZone::Atlantic::South_Georgia 2.62
+ DateTime::TimeZone::Atlantic::Stanley 2.62
+ DateTime::TimeZone::Australia::Adelaide 2.62
+ DateTime::TimeZone::Australia::Brisbane 2.62
+ DateTime::TimeZone::Australia::Broken_Hill 2.62
+ DateTime::TimeZone::Australia::Darwin 2.62
+ DateTime::TimeZone::Australia::Eucla 2.62
+ DateTime::TimeZone::Australia::Hobart 2.62
+ DateTime::TimeZone::Australia::Lindeman 2.62
+ DateTime::TimeZone::Australia::Lord_Howe 2.62
+ DateTime::TimeZone::Australia::Melbourne 2.62
+ DateTime::TimeZone::Australia::Perth 2.62
+ DateTime::TimeZone::Australia::Sydney 2.62
+ DateTime::TimeZone::CET 2.62
+ DateTime::TimeZone::CST6CDT 2.62
+ DateTime::TimeZone::Catalog 2.62
+ DateTime::TimeZone::EET 2.62
+ DateTime::TimeZone::EST 2.62
+ DateTime::TimeZone::EST5EDT 2.62
+ DateTime::TimeZone::Europe::Andorra 2.62
+ DateTime::TimeZone::Europe::Astrakhan 2.62
+ DateTime::TimeZone::Europe::Athens 2.62
+ DateTime::TimeZone::Europe::Belgrade 2.62
+ DateTime::TimeZone::Europe::Berlin 2.62
+ DateTime::TimeZone::Europe::Brussels 2.62
+ DateTime::TimeZone::Europe::Bucharest 2.62
+ DateTime::TimeZone::Europe::Budapest 2.62
+ DateTime::TimeZone::Europe::Chisinau 2.62
+ DateTime::TimeZone::Europe::Dublin 2.62
+ DateTime::TimeZone::Europe::Gibraltar 2.62
+ DateTime::TimeZone::Europe::Helsinki 2.62
+ DateTime::TimeZone::Europe::Istanbul 2.62
+ DateTime::TimeZone::Europe::Kaliningrad 2.62
+ DateTime::TimeZone::Europe::Kirov 2.62
+ DateTime::TimeZone::Europe::Kyiv 2.62
+ DateTime::TimeZone::Europe::Lisbon 2.62
+ DateTime::TimeZone::Europe::London 2.62
+ DateTime::TimeZone::Europe::Madrid 2.62
+ DateTime::TimeZone::Europe::Malta 2.62
+ DateTime::TimeZone::Europe::Minsk 2.62
+ DateTime::TimeZone::Europe::Moscow 2.62
+ DateTime::TimeZone::Europe::Paris 2.62
+ DateTime::TimeZone::Europe::Prague 2.62
+ DateTime::TimeZone::Europe::Riga 2.62
+ DateTime::TimeZone::Europe::Rome 2.62
+ DateTime::TimeZone::Europe::Samara 2.62
+ DateTime::TimeZone::Europe::Saratov 2.62
+ DateTime::TimeZone::Europe::Simferopol 2.62
+ DateTime::TimeZone::Europe::Sofia 2.62
+ DateTime::TimeZone::Europe::Tallinn 2.62
+ DateTime::TimeZone::Europe::Tirane 2.62
+ DateTime::TimeZone::Europe::Ulyanovsk 2.62
+ DateTime::TimeZone::Europe::Vienna 2.62
+ DateTime::TimeZone::Europe::Vilnius 2.62
+ DateTime::TimeZone::Europe::Volgograd 2.62
+ DateTime::TimeZone::Europe::Warsaw 2.62
+ DateTime::TimeZone::Europe::Zurich 2.62
+ DateTime::TimeZone::Floating 2.62
+ DateTime::TimeZone::HST 2.62
+ DateTime::TimeZone::Indian::Chagos 2.62
+ DateTime::TimeZone::Indian::Maldives 2.62
+ DateTime::TimeZone::Indian::Mauritius 2.62
+ DateTime::TimeZone::Local 2.62
+ DateTime::TimeZone::Local::Android 2.62
+ DateTime::TimeZone::Local::Unix 2.62
+ DateTime::TimeZone::Local::VMS 2.62
+ DateTime::TimeZone::MET 2.62
+ DateTime::TimeZone::MST 2.62
+ DateTime::TimeZone::MST7MDT 2.62
+ DateTime::TimeZone::OffsetOnly 2.62
+ DateTime::TimeZone::OlsonDB 2.62
+ DateTime::TimeZone::OlsonDB::Change 2.62
+ DateTime::TimeZone::OlsonDB::Observance 2.62
+ DateTime::TimeZone::OlsonDB::Rule 2.62
+ DateTime::TimeZone::OlsonDB::Zone 2.62
+ DateTime::TimeZone::PST8PDT 2.62
+ DateTime::TimeZone::Pacific::Apia 2.62
+ DateTime::TimeZone::Pacific::Auckland 2.62
+ DateTime::TimeZone::Pacific::Bougainville 2.62
+ DateTime::TimeZone::Pacific::Chatham 2.62
+ DateTime::TimeZone::Pacific::Easter 2.62
+ DateTime::TimeZone::Pacific::Efate 2.62
+ DateTime::TimeZone::Pacific::Fakaofo 2.62
+ DateTime::TimeZone::Pacific::Fiji 2.62
+ DateTime::TimeZone::Pacific::Galapagos 2.62
+ DateTime::TimeZone::Pacific::Gambier 2.62
+ DateTime::TimeZone::Pacific::Guadalcanal 2.62
+ DateTime::TimeZone::Pacific::Guam 2.62
+ DateTime::TimeZone::Pacific::Honolulu 2.62
+ DateTime::TimeZone::Pacific::Kanton 2.62
+ DateTime::TimeZone::Pacific::Kiritimati 2.62
+ DateTime::TimeZone::Pacific::Kosrae 2.62
+ DateTime::TimeZone::Pacific::Kwajalein 2.62
+ DateTime::TimeZone::Pacific::Marquesas 2.62
+ DateTime::TimeZone::Pacific::Nauru 2.62
+ DateTime::TimeZone::Pacific::Niue 2.62
+ DateTime::TimeZone::Pacific::Norfolk 2.62
+ DateTime::TimeZone::Pacific::Noumea 2.62
+ DateTime::TimeZone::Pacific::Pago_Pago 2.62
+ DateTime::TimeZone::Pacific::Palau 2.62
+ DateTime::TimeZone::Pacific::Pitcairn 2.62
+ DateTime::TimeZone::Pacific::Port_Moresby 2.62
+ DateTime::TimeZone::Pacific::Rarotonga 2.62
+ DateTime::TimeZone::Pacific::Tahiti 2.62
+ DateTime::TimeZone::Pacific::Tarawa 2.62
+ DateTime::TimeZone::Pacific::Tongatapu 2.62
+ DateTime::TimeZone::UTC 2.62
+ DateTime::TimeZone::WET 2.62
requirements:
Class::Singleton 1.03
Cwd 3
@@ -913,19 +946,11 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
- Devel-GlobalDestruction-0.14
- pathname: H/HA/HAARG/Devel-GlobalDestruction-0.14.tar.gz
+ Devel-StackTrace-2.05
+ pathname: D/DR/DROLSKY/Devel-StackTrace-2.05.tar.gz
provides:
- Devel::GlobalDestruction 0.14
- requirements:
- ExtUtils::MakeMaker 0
- Sub::Exporter::Progressive 0.001011
- perl 5.006
- Devel-StackTrace-2.04
- pathname: D/DR/DROLSKY/Devel-StackTrace-2.04.tar.gz
- provides:
- Devel::StackTrace 2.04
- Devel::StackTrace::Frame 2.04
+ Devel::StackTrace 2.05
+ Devel::StackTrace::Frame 2.05
requirements:
ExtUtils::MakeMaker 0
File::Spec 0
@@ -946,78 +971,83 @@ DISTRIBUTIONS
base 0
strict 0
warnings 0
- Email-Abstract-3.008
- pathname: R/RJ/RJBS/Email-Abstract-3.008.tar.gz
+ Email-Abstract-3.010
+ pathname: R/RJ/RJBS/Email-Abstract-3.010.tar.gz
provides:
- Email::Abstract 3.008
- Email::Abstract::EmailMIME 3.008
- Email::Abstract::EmailSimple 3.008
- Email::Abstract::MIMEEntity 3.008
- Email::Abstract::MailInternet 3.008
- Email::Abstract::MailMessage 3.008
- Email::Abstract::Plugin 3.008
+ Email::Abstract 3.010
+ Email::Abstract::EmailMIME 3.010
+ Email::Abstract::EmailSimple 3.010
+ Email::Abstract::MIMEEntity 3.010
+ Email::Abstract::MailInternet 3.010
+ Email::Abstract::MailMessage 3.010
+ Email::Abstract::Plugin 3.010
requirements:
Carp 0
Email::Simple 1.998
- ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker 6.78
MRO::Compat 0
Module::Pluggable 1.5
Scalar::Util 0
perl 5.006
strict 0
warnings 0
- Email-Address-1.912
- pathname: R/RJ/RJBS/Email-Address-1.912.tar.gz
+ Email-Address-XS-1.05
+ pathname: P/PA/PALI/Email-Address-XS-1.05.tar.gz
provides:
- Email::Address 1.912
+ Email::Address::XS 1.05
requirements:
+ Carp 0
+ Exporter 0
ExtUtils::MakeMaker 0
+ XSLoader 0
+ base 0
overload 0
+ perl 5.006000
strict 0
warnings 0
- Email-Date-Format-1.005
- pathname: R/RJ/RJBS/Email-Date-Format-1.005.tar.gz
+ Email-Date-Format-1.008
+ pathname: R/RJ/RJBS/Email-Date-Format-1.008.tar.gz
provides:
- Email::Date::Format 1.005
+ Email::Date::Format 1.008
requirements:
Exporter 5.57
- ExtUtils::MakeMaker 0
- Time::Local 0
- strict 0
+ ExtUtils::MakeMaker 6.78
+ Time::Local 1.27
+ perl 5.012
warnings 0
- Email-Sender-1.300031
- pathname: R/RJ/RJBS/Email-Sender-1.300031.tar.gz
- provides:
- Email::Sender 1.300031
- Email::Sender::Failure 1.300031
- Email::Sender::Failure::Multi 1.300031
- Email::Sender::Failure::Permanent 1.300031
- Email::Sender::Failure::Temporary 1.300031
- Email::Sender::Manual 1.300031
- Email::Sender::Manual::QuickStart 1.300031
- Email::Sender::Role::CommonSending 1.300031
- Email::Sender::Role::HasMessage 1.300031
- Email::Sender::Simple 1.300031
- Email::Sender::Success 1.300031
- Email::Sender::Success::Partial 1.300031
- Email::Sender::Transport 1.300031
- Email::Sender::Transport::DevNull 1.300031
- Email::Sender::Transport::Failable 1.300031
- Email::Sender::Transport::Maildir 1.300031
- Email::Sender::Transport::Mbox 1.300031
- Email::Sender::Transport::Print 1.300031
- Email::Sender::Transport::SMTP 1.300031
- Email::Sender::Transport::SMTP::Persistent 1.300031
- Email::Sender::Transport::Sendmail 1.300031
- Email::Sender::Transport::Test 1.300031
- Email::Sender::Transport::Wrapper 1.300031
- Email::Sender::Util 1.300031
+ Email-Sender-2.601
+ pathname: R/RJ/RJBS/Email-Sender-2.601.tar.gz
+ provides:
+ Email::Sender 2.601
+ Email::Sender::Failure 2.601
+ Email::Sender::Failure::Multi 2.601
+ Email::Sender::Failure::Permanent 2.601
+ Email::Sender::Failure::Temporary 2.601
+ Email::Sender::Manual 2.601
+ Email::Sender::Manual::QuickStart 2.601
+ Email::Sender::Role::CommonSending 2.601
+ Email::Sender::Role::HasMessage 2.601
+ Email::Sender::Simple 2.601
+ Email::Sender::Success 2.601
+ Email::Sender::Success::Partial 2.601
+ Email::Sender::Transport 2.601
+ Email::Sender::Transport::DevNull 2.601
+ Email::Sender::Transport::Failable 2.601
+ Email::Sender::Transport::Maildir 2.601
+ Email::Sender::Transport::Mbox 2.601
+ Email::Sender::Transport::Print 2.601
+ Email::Sender::Transport::SMTP 2.601
+ Email::Sender::Transport::SMTP::Persistent 2.601
+ Email::Sender::Transport::Sendmail 2.601
+ Email::Sender::Transport::Test 2.601
+ Email::Sender::Transport::Wrapper 2.601
+ Email::Sender::Util 2.601
requirements:
Carp 0
Email::Abstract 3.006
- Email::Address 0
+ Email::Address::XS 0
Email::Simple 1.998
- ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker 6.78
Fcntl 0
File::Basename 0
File::Path 2.06
@@ -1037,20 +1067,21 @@ DISTRIBUTIONS
Sys::Hostname 0
Throwable::Error 0.200003
Try::Tiny 0
+ perl 5.012
strict 0
utf8 0
warnings 0
- Email-Simple-2.216
- pathname: R/RJ/RJBS/Email-Simple-2.216.tar.gz
+ Email-Simple-2.218
+ pathname: R/RJ/RJBS/Email-Simple-2.218.tar.gz
provides:
- Email::Simple 2.216
- Email::Simple::Creator 2.216
- Email::Simple::Header 2.216
+ Email::Simple 2.218
+ Email::Simple::Creator 2.218
+ Email::Simple::Header 2.218
requirements:
Carp 0
Email::Date::Format 0
- ExtUtils::MakeMaker 0
- perl 5.008
+ ExtUtils::MakeMaker 6.78
+ perl 5.012
strict 0
warnings 0
Encode-Locale-1.05
@@ -1075,12 +1106,13 @@ DISTRIBUTIONS
overload 0
strict 0
warnings 0
- Exception-Class-1.44
- pathname: D/DR/DROLSKY/Exception-Class-1.44.tar.gz
+ Exception-Class-1.45
+ pathname: D/DR/DROLSKY/Exception-Class-1.45.tar.gz
provides:
- Exception::Class 1.44
- Exception::Class::Base 1.44
+ Exception::Class 1.45
+ Exception::Class::Base 1.45
requirements:
+ Carp 0
Class::Data::Inheritable 0.02
Devel::StackTrace 2.00
ExtUtils::MakeMaker 0
@@ -1090,11 +1122,11 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- Exporter-Tiny-1.002001
- pathname: T/TO/TOBYINK/Exporter-Tiny-1.002001.tar.gz
+ Exporter-Tiny-1.006002
+ pathname: T/TO/TOBYINK/Exporter-Tiny-1.006002.tar.gz
provides:
- Exporter::Shiny 1.002001
- Exporter::Tiny 1.002001
+ Exporter::Shiny 1.006002
+ Exporter::Tiny 1.006002
requirements:
ExtUtils::MakeMaker 6.17
perl 5.006001
@@ -1107,13 +1139,13 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 6.30
strict 0
warnings 0
- ExtUtils-Depends-0.8000
- pathname: X/XA/XAOC/ExtUtils-Depends-0.8000.tar.gz
+ ExtUtils-Depends-0.8001
+ pathname: X/XA/XAOC/ExtUtils-Depends-0.8001.tar.gz
provides:
- ExtUtils::Depends 0.8000
+ ExtUtils::Depends 0.8001
requirements:
Data::Dumper 0
- ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker 7.44
File::Spec 0
IO::File 0
perl 5.006
@@ -1147,36 +1179,39 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- FFI-CheckLib-0.24
- pathname: P/PL/PLICEASE/FFI-CheckLib-0.24.tar.gz
+ FFI-CheckLib-0.31
+ pathname: P/PL/PLICEASE/FFI-CheckLib-0.31.tar.gz
provides:
- FFI::CheckLib 0.24
+ FFI::CheckLib 0.31
requirements:
ExtUtils::MakeMaker 0
+ File::Which 0
+ List::Util 1.33
perl 5.006
- File-Listing-6.04
- pathname: G/GA/GAAS/File-Listing-6.04.tar.gz
+ File-Listing-6.16
+ pathname: P/PL/PLICEASE/File-Listing-6.16.tar.gz
provides:
- File::Listing 6.04
- File::Listing::apache 6.04
- File::Listing::dosftp 6.04
- File::Listing::netware 6.04
- File::Listing::unix 6.04
- File::Listing::vms 6.04
+ File::Listing 6.16
+ File::Listing::apache 6.16
+ File::Listing::dosftp 6.16
+ File::Listing::netware 6.16
+ File::Listing::unix 6.16
+ File::Listing::vms 6.16
requirements:
+ Exporter 5.57
ExtUtils::MakeMaker 0
- HTTP::Date 6
- perl 5.006002
+ HTTP::Date 0
+ perl 5.006
File-NFSLock-1.29
pathname: B/BB/BBB/File-NFSLock-1.29.tar.gz
provides:
File::NFSLock 1.29
requirements:
ExtUtils::MakeMaker 0
- File-ShareDir-1.116
- pathname: R/RE/REHSACK/File-ShareDir-1.116.tar.gz
+ File-ShareDir-1.118
+ pathname: R/RE/REHSACK/File-ShareDir-1.118.tar.gz
provides:
- File::ShareDir 1.116
+ File::ShareDir 1.118
requirements:
Carp 0
Class::Inspector 1.12
@@ -1185,10 +1220,10 @@ DISTRIBUTIONS
File::Spec 0.80
perl 5.008001
warnings 0
- File-ShareDir-Install-0.13
- pathname: E/ET/ETHER/File-ShareDir-Install-0.13.tar.gz
+ File-ShareDir-Install-0.14
+ pathname: E/ET/ETHER/File-ShareDir-Install-0.14.tar.gz
provides:
- File::ShareDir::Install 0.13
+ File::ShareDir::Install 0.14
requirements:
Carp 0
Exporter 0
@@ -1198,19 +1233,38 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- File-Which-1.23
- pathname: P/PL/PLICEASE/File-Which-1.23.tar.gz
+ File-Slurp-9999.32
+ pathname: C/CA/CAPOEIRAB/File-Slurp-9999.32.tar.gz
provides:
- File::Which 1.23
+ File::Slurp 9999.32
requirements:
+ B 0
+ Carp 0
+ Errno 0
+ Exporter 5.57
ExtUtils::MakeMaker 0
+ Fcntl 0
+ File::Basename 0
+ File::Spec 3.01
+ File::Temp 0
+ IO::Handle 0
+ POSIX 0
+ strict 0
+ warnings 0
+ File-Which-1.27
+ pathname: P/PL/PLICEASE/File-Which-1.27.tar.gz
+ provides:
+ File::Which 1.27
+ requirements:
+ ExtUtils::MakeMaker 0
+ base 0
perl 5.006
- File-chdir-0.1010
- pathname: D/DA/DAGOLDEN/File-chdir-0.1010.tar.gz
+ File-chdir-0.1011
+ pathname: D/DA/DAGOLDEN/File-chdir-0.1011.tar.gz
provides:
- File::chdir 0.1010
- File::chdir::ARRAY 0.1010
- File::chdir::SCALAR 0.1010
+ File::chdir 0.1011
+ File::chdir::ARRAY 0.1011
+ File::chdir::SCALAR 0.1011
requirements:
Carp 0
Cwd 3.16
@@ -1220,20 +1274,20 @@ DISTRIBUTIONS
perl 5.006
strict 0
vars 0
- GIS-Distance-0.18
- pathname: B/BL/BLUEFEET/GIS-Distance-0.18.tar.gz
- provides:
- GIS::Distance 0.18
- GIS::Distance::ALT 0.18
- GIS::Distance::Constants 0.18
- GIS::Distance::Cosine 0.18
- GIS::Distance::Formula 0.18
- GIS::Distance::GreatCircle 0.18
- GIS::Distance::Haversine 0.18
- GIS::Distance::MathTrig 0.18
- GIS::Distance::Null 0.18
- GIS::Distance::Polar 0.18
- GIS::Distance::Vincenty 0.18
+ GIS-Distance-0.20
+ pathname: B/BL/BLUEFEET/GIS-Distance-0.20.tar.gz
+ provides:
+ GIS::Distance 0.20
+ GIS::Distance::ALT 0.20
+ GIS::Distance::Constants 0.20
+ GIS::Distance::Cosine 0.20
+ GIS::Distance::Formula 0.20
+ GIS::Distance::GreatCircle 0.20
+ GIS::Distance::Haversine 0.20
+ GIS::Distance::MathTrig 0.20
+ GIS::Distance::Null 0.20
+ GIS::Distance::Polar 0.20
+ GIS::Distance::Vincenty 0.20
requirements:
Carp 0
Class::Measure::Length 0
@@ -1245,125 +1299,113 @@ DISTRIBUTIONS
parent 0
perl 5.008001
strictures 2.000000
- Geo-Distance-0.24
- pathname: B/BL/BLUEFEET/Geo-Distance-0.24.tar.gz
- provides:
- Geo::Distance 0.24
- requirements:
- Carp 0
- Const::Fast 0.014
- GIS::Distance 0.14
- GIS::Distance::Constants 0.14
+ GIS-Distance-Fast-0.16
+ pathname: B/BL/BLUEFEET/GIS-Distance-Fast-0.16.tar.gz
+ provides:
+ GIS::Distance::Fast 0.16
+ GIS::Distance::Fast::ALT 0.16
+ GIS::Distance::Fast::Cosine 0.16
+ GIS::Distance::Fast::GreatCircle 0.16
+ GIS::Distance::Fast::Haversine 0.16
+ GIS::Distance::Fast::Null 0.16
+ GIS::Distance::Fast::Polar 0.16
+ GIS::Distance::Fast::Vincenty 0.16
+ requirements:
+ GIS::Distance::Formula 0.17
Module::Build::Tiny 0.035
+ namespace::clean 0.24
+ parent 0
perl 5.008001
- Geo-Distance-XS-0.13
- pathname: G/GR/GRAY/Geo-Distance-XS-0.13.tar.gz
+ strictures 2.000000
+ HTML-Parser-3.82
+ pathname: O/OA/OALDERS/HTML-Parser-3.82.tar.gz
provides:
- Geo::Distance::XS 0.13
+ HTML::Entities 3.82
+ HTML::Filter 3.82
+ HTML::HeadParser 3.82
+ HTML::LinkExtor 3.82
+ HTML::Parser 3.82
+ HTML::PullParser 3.82
+ HTML::TokeParser 3.82
requirements:
- ExtUtils::MakeMaker 0
- Geo::Distance 0.16
- Test::More 0.82
- XSLoader 0
- HTML-Parser-3.72
- pathname: G/GA/GAAS/HTML-Parser-3.72.tar.gz
- provides:
- HTML::Entities 3.69
- HTML::Filter 3.72
- HTML::HeadParser 3.71
- HTML::LinkExtor 3.69
- HTML::Parser 3.72
- HTML::PullParser 3.57
- HTML::TokeParser 3.69
- requirements:
- ExtUtils::MakeMaker 0
- HTML::Tagset 3
+ Carp 0
+ Exporter 0
+ ExtUtils::MakeMaker 6.52
+ HTML::Tagset 0
+ HTTP::Headers 0
+ IO::File 0
+ URI 0
+ URI::URL 0
XSLoader 0
- perl 5.008
- HTML-Tagset-3.20
- pathname: P/PE/PETDANCE/HTML-Tagset-3.20.tar.gz
+ strict 0
+ HTML-Tagset-3.24
+ pathname: P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz
provides:
- HTML::Tagset 3.20
+ HTML::Tagset 3.24
requirements:
- ExtUtils::MakeMaker 0
- HTTP-Cookies-6.04
- pathname: O/OA/OALDERS/HTTP-Cookies-6.04.tar.gz
+ ExtUtils::MakeMaker 6.46
+ perl 5.010001
+ HTTP-Cookies-6.11
+ pathname: O/OA/OALDERS/HTTP-Cookies-6.11.tar.gz
provides:
- HTTP::Cookies 6.04
- HTTP::Cookies::Microsoft 6.04
- HTTP::Cookies::Netscape 6.04
+ HTTP::Cookies 6.11
+ HTTP::Cookies::Microsoft 6.11
+ HTTP::Cookies::Netscape 6.11
requirements:
Carp 0
ExtUtils::MakeMaker 0
HTTP::Date 6
HTTP::Headers::Util 6
HTTP::Request 0
- Time::Local 0
locale 0
perl 5.008001
strict 0
- vars 0
- HTTP-Daemon-6.04
- pathname: O/OA/OALDERS/HTTP-Daemon-6.04.tar.gz
- provides:
- HTTP::Daemon 6.04
- requirements:
- Carp 0
- ExtUtils::MakeMaker 0
- HTTP::Date 6
- HTTP::Request 6
- HTTP::Response 6
- HTTP::Status 6
- IO::Socket 0
- LWP::MediaTypes 6
- Module::Build::Tiny 0.034
- Sys::Hostname 0
- perl 5.006
- strict 0
- warnings 0
- HTTP-Date-6.02
- pathname: G/GA/GAAS/HTTP-Date-6.02.tar.gz
+ HTTP-Date-6.06
+ pathname: O/OA/OALDERS/HTTP-Date-6.06.tar.gz
provides:
- HTTP::Date 6.02
+ HTTP::Date 6.06
requirements:
+ Exporter 0
ExtUtils::MakeMaker 0
- Time::Local 0
+ Time::Local 1.28
+ Time::Zone 0
perl 5.006002
- HTTP-Message-6.18
- pathname: O/OA/OALDERS/HTTP-Message-6.18.tar.gz
- provides:
- HTTP::Config 6.18
- HTTP::Headers 6.18
- HTTP::Headers::Auth 6.18
- HTTP::Headers::ETag 6.18
- HTTP::Headers::Util 6.18
- HTTP::Message 6.18
- HTTP::Request 6.18
- HTTP::Request::Common 6.18
- HTTP::Response 6.18
- HTTP::Status 6.18
+ strict 0
+ HTTP-Message-6.45
+ pathname: O/OA/OALDERS/HTTP-Message-6.45.tar.gz
+ provides:
+ HTTP::Config 6.45
+ HTTP::Headers 6.45
+ HTTP::Headers::Auth 6.45
+ HTTP::Headers::ETag 6.45
+ HTTP::Headers::Util 6.45
+ HTTP::Message 6.45
+ HTTP::Request 6.45
+ HTTP::Request::Common 6.45
+ HTTP::Response 6.45
+ HTTP::Status 6.45
requirements:
Carp 0
- Compress::Raw::Zlib 0
- Encode 2.21
+ Clone 0.46
+ Compress::Raw::Bzip2 0
+ Compress::Raw::Zlib 2.062
+ Encode 3.01
Encode::Locale 1
Exporter 5.57
ExtUtils::MakeMaker 0
+ File::Spec 0
HTTP::Date 6
IO::Compress::Bzip2 2.021
IO::Compress::Deflate 0
IO::Compress::Gzip 0
IO::HTML 0
- IO::Uncompress::Bunzip2 2.021
- IO::Uncompress::Gunzip 0
IO::Uncompress::Inflate 0
IO::Uncompress::RawInflate 0
LWP::MediaTypes 6
MIME::Base64 2.1
MIME::QuotedPrint 0
- Storable 0
URI 1.10
- base 0
+ parent 0
perl 5.008001
strict 0
warnings 0
@@ -1375,10 +1417,10 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
HTTP::Headers 6
perl 5.008001
- Hash-Merge-0.300
- pathname: R/RE/REHSACK/Hash-Merge-0.300.tar.gz
+ Hash-Merge-0.302
+ pathname: H/HE/HERMES/Hash-Merge-0.302.tar.gz
provides:
- Hash::Merge 0.300
+ Hash::Merge 0.302
requirements:
Clone::Choose 0.008
ExtUtils::MakeMaker 6.64
@@ -1401,33 +1443,52 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Test::Simple 0.45
- IO-HTML-1.001
- pathname: C/CJ/CJM/IO-HTML-1.001.tar.gz
+ IO-HTML-1.004
+ pathname: C/CJ/CJM/IO-HTML-1.004.tar.gz
provides:
- IO::HTML 1.001
+ IO::HTML 1.004
requirements:
Carp 0
Encode 2.10
Exporter 5.57
- ExtUtils::MakeMaker 6.30
+ ExtUtils::MakeMaker 0
+ perl 5.008
+ IO-Socket-SSL-2.085
+ pathname: S/SU/SULLR/IO-Socket-SSL-2.085.tar.gz
+ provides:
+ IO::Socket::SSL 2.085
+ IO::Socket::SSL::Intercept 2.056
+ IO::Socket::SSL::OCSP_Cache 2.085
+ IO::Socket::SSL::OCSP_Resolver 2.085
+ IO::Socket::SSL::PublicSuffix undef
+ IO::Socket::SSL::SSL_Context 2.085
+ IO::Socket::SSL::SSL_HANDLE 2.085
+ IO::Socket::SSL::Session_Cache 2.085
+ IO::Socket::SSL::Trace 2.085
+ IO::Socket::SSL::Utils 2.015
+ requirements:
+ ExtUtils::MakeMaker 0
+ Mozilla::CA 0
+ Net::SSLeay 1.46
+ Scalar::Util 0
IO-String-1.08
pathname: G/GA/GAAS/IO-String-1.08.tar.gz
provides:
IO::String 1.08
requirements:
ExtUtils::MakeMaker 0
- JSON-4.02
- pathname: I/IS/ISHIGAKI/JSON-4.02.tar.gz
+ JSON-4.10
+ pathname: I/IS/ISHIGAKI/JSON-4.10.tar.gz
provides:
- JSON 4.02
- JSON::Backend::PP 4.02
+ JSON 4.10
+ JSON::Backend::PP 4.10
requirements:
ExtUtils::MakeMaker 0
Test::More 0
- JSON-XS-4.02
- pathname: M/ML/MLEHMANN/JSON-XS-4.02.tar.gz
+ JSON-XS-4.03
+ pathname: M/ML/MLEHMANN/JSON-XS-4.03.tar.gz
provides:
- JSON::XS 4.02
+ JSON::XS 4.03
requirements:
Canary::Stability 0
ExtUtils::MakeMaker 6.52
@@ -1445,31 +1506,45 @@ DISTRIBUTIONS
Scalar::Util 0
perl 5.006002
strict 0
- List-Compare-0.53
- pathname: J/JK/JKEENAN/List-Compare-0.53.tar.gz
+ LWP-Protocol-https-6.14
+ pathname: O/OA/OALDERS/LWP-Protocol-https-6.14.tar.gz
+ provides:
+ LWP::Protocol::https 6.14
+ LWP::Protocol::https::Socket 6.14
+ requirements:
+ ExtUtils::MakeMaker 0
+ IO::Socket::SSL 1.970
+ LWP::Protocol::http 0
+ LWP::UserAgent 6.06
+ Net::HTTPS 6
+ base 0
+ perl 5.008001
+ strict 0
+ List-Compare-0.55
+ pathname: J/JK/JKEENAN/List-Compare-0.55.tar.gz
provides:
- List::Compare 0.53
- List::Compare::Accelerated 0.53
- List::Compare::Base::_Auxiliary 0.53
- List::Compare::Base::_Engine 0.53
- List::Compare::Functional 0.53
- List::Compare::Multiple 0.53
- List::Compare::Multiple::Accelerated 0.53
+ List::Compare 0.55
+ List::Compare::Accelerated 0.55
+ List::Compare::Base::_Auxiliary 0.55
+ List::Compare::Base::_Engine 0.55
+ List::Compare::Functional 0.55
+ List::Compare::Multiple 0.55
+ List::Compare::Multiple::Accelerated 0.55
requirements:
ExtUtils::MakeMaker 0
- List-MoreUtils-0.428
- pathname: R/RE/REHSACK/List-MoreUtils-0.428.tar.gz
+ List-MoreUtils-0.430
+ pathname: R/RE/REHSACK/List-MoreUtils-0.430.tar.gz
provides:
- List::MoreUtils 0.428
- List::MoreUtils::PP 0.428
+ List::MoreUtils 0.430
+ List::MoreUtils::PP 0.430
requirements:
Exporter::Tiny 0.038
ExtUtils::MakeMaker 0
- List::MoreUtils::XS 0.426
- List-MoreUtils-XS-0.428
- pathname: R/RE/REHSACK/List-MoreUtils-XS-0.428.tar.gz
+ List::MoreUtils::XS 0.430
+ List-MoreUtils-XS-0.430
+ pathname: R/RE/REHSACK/List-MoreUtils-XS-0.430.tar.gz
provides:
- List::MoreUtils::XS 0.428
+ List::MoreUtils::XS 0.430
requirements:
Carp 0
ExtUtils::MakeMaker 0
@@ -1480,42 +1555,122 @@ DISTRIBUTIONS
IPC::Cmd 0
XSLoader 0.22
base 0
- List-UtilsBy-0.11
- pathname: P/PE/PEVANS/List-UtilsBy-0.11.tar.gz
+ List-UtilsBy-0.12
+ pathname: P/PE/PEVANS/List-UtilsBy-0.12.tar.gz
provides:
- List::UtilsBy 0.11
+ List::UtilsBy 0.12
requirements:
Exporter 5.57
Module::Build 0.4004
- MRO-Compat-0.13
- pathname: H/HA/HAARG/MRO-Compat-0.13.tar.gz
+ MIME-tools-5.514
+ pathname: D/DS/DSKOLL/MIME-tools-5.514.tar.gz
+ provides:
+ MIME::Body 5.514
+ MIME::Body::File 5.514
+ MIME::Body::InCore 5.514
+ MIME::Body::Scalar 5.514
+ MIME::Decoder 5.514
+ MIME::Decoder::Base64 5.514
+ MIME::Decoder::BinHex 5.514
+ MIME::Decoder::Binary 5.514
+ MIME::Decoder::Gzip64 5.514
+ MIME::Decoder::NBit 5.514
+ MIME::Decoder::QuotedPrint 5.514
+ MIME::Decoder::UU 5.514
+ MIME::Entity 5.514
+ MIME::Field::ConTraEnc 5.514
+ MIME::Field::ContDisp 5.514
+ MIME::Field::ContType 5.514
+ MIME::Field::ParamVal 5.514
+ MIME::Head 5.514
+ MIME::Parser 5.514
+ MIME::Parser::FileInto undef
+ MIME::Parser::FileUnder undef
+ MIME::Parser::Filer undef
+ MIME::Parser::Reader undef
+ MIME::Parser::Results undef
+ MIME::Tools 5.514
+ MIME::WordDecoder undef
+ MIME::WordDecoder::ISO_8859 undef
+ MIME::WordDecoder::US_ASCII undef
+ MIME::WordDecoder::UTF_8 undef
+ MIME::Words 5.514
+ requirements:
+ ExtUtils::MakeMaker 6.59
+ File::Path 1
+ File::Spec 0.6
+ File::Temp 0.18
+ IO::File 1.13
+ IO::Handle 0
+ MIME::Base64 2.2
+ Mail::Field 1.05
+ Mail::Header 1.01
+ Mail::Internet 1.0203
+ Test::Deep 0
+ Test::More 0
+ perl 5.008
+ MRO-Compat-0.15
+ pathname: H/HA/HAARG/MRO-Compat-0.15.tar.gz
provides:
- MRO::Compat 0.13
+ MRO::Compat 0.15
requirements:
ExtUtils::MakeMaker 0
perl 5.006
- Module-Build-0.4229
- pathname: L/LE/LEONT/Module-Build-0.4229.tar.gz
- provides:
- Module::Build 0.4229
- Module::Build::Base 0.4229
- Module::Build::Compat 0.4229
- Module::Build::Config 0.4229
- Module::Build::Cookbook 0.4229
- Module::Build::Dumper 0.4229
- Module::Build::Notes 0.4229
- Module::Build::PPMMaker 0.4229
- Module::Build::Platform::Default 0.4229
- Module::Build::Platform::MacOS 0.4229
- Module::Build::Platform::Unix 0.4229
- Module::Build::Platform::VMS 0.4229
- Module::Build::Platform::VOS 0.4229
- Module::Build::Platform::Windows 0.4229
- Module::Build::Platform::aix 0.4229
- Module::Build::Platform::cygwin 0.4229
- Module::Build::Platform::darwin 0.4229
- Module::Build::Platform::os2 0.4229
- Module::Build::PodParser 0.4229
+ MailTools-2.21
+ pathname: M/MA/MARKOV/MailTools-2.21.tar.gz
+ provides:
+ Mail::Address 2.21
+ Mail::Cap 2.21
+ Mail::Field 2.21
+ Mail::Field::AddrList 2.21
+ Mail::Field::Date 2.21
+ Mail::Field::Generic 2.21
+ Mail::Filter 2.21
+ Mail::Header 2.21
+ Mail::Internet 2.21
+ Mail::Mailer 2.21
+ Mail::Mailer::qmail 2.21
+ Mail::Mailer::rfc822 2.21
+ Mail::Mailer::sendmail 2.21
+ Mail::Mailer::smtp 2.21
+ Mail::Mailer::smtp::pipe 2.21
+ Mail::Mailer::smtps 2.21
+ Mail::Mailer::smtps::pipe 2.21
+ Mail::Mailer::testfile 2.21
+ Mail::Mailer::testfile::pipe 2.21
+ Mail::Send 2.21
+ Mail::Util 2.21
+ MailTools 2.21
+ requirements:
+ Date::Format 0
+ Date::Parse 0
+ ExtUtils::MakeMaker 0
+ IO::Handle 0
+ Net::Domain 1.05
+ Net::SMTP 1.03
+ Test::More 0
+ Module-Build-0.4234
+ pathname: L/LE/LEONT/Module-Build-0.4234.tar.gz
+ provides:
+ Module::Build 0.4234
+ Module::Build::Base 0.4234
+ Module::Build::Compat 0.4234
+ Module::Build::Config 0.4234
+ Module::Build::Cookbook 0.4234
+ Module::Build::Dumper 0.4234
+ Module::Build::Notes 0.4234
+ Module::Build::PPMMaker 0.4234
+ Module::Build::Platform::Default 0.4234
+ Module::Build::Platform::MacOS 0.4234
+ Module::Build::Platform::Unix 0.4234
+ Module::Build::Platform::VMS 0.4234
+ Module::Build::Platform::VOS 0.4234
+ Module::Build::Platform::Windows 0.4234
+ Module::Build::Platform::aix 0.4234
+ Module::Build::Platform::cygwin 0.4234
+ Module::Build::Platform::darwin 0.4234
+ Module::Build::Platform::os2 0.4234
+ Module::Build::PodParser 0.4234
requirements:
CPAN::Meta 2.142060
Cwd 0
@@ -1534,16 +1689,15 @@ DISTRIBUTIONS
Getopt::Long 0
Module::Metadata 1.000002
Perl::OSType 1
- Pod::Man 2.17
TAP::Harness 3.29
Text::Abbrev 0
Text::ParseWords 0
perl 5.006001
version 0.87
- Module-Build-Tiny-0.039
- pathname: L/LE/LEONT/Module-Build-Tiny-0.039.tar.gz
+ Module-Build-Tiny-0.047
+ pathname: L/LE/LEONT/Module-Build-Tiny-0.047.tar.gz
provides:
- Module::Build::Tiny 0.039
+ Module::Build::Tiny 0.047
requirements:
CPAN::Meta 0
DynaLoader 0
@@ -1590,7 +1744,7 @@ DISTRIBUTIONS
File::Spec 3.00
File::Spec::Functions 0
if 0
- perl 5.00503
+ perl 5.005030
strict 0
Module-Runtime-0.016
pathname: Z/ZE/ZEFRAM/Module-Runtime-0.016.tar.gz
@@ -1602,24 +1756,23 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- Mojo-Pg-4.13
- pathname: S/SR/SRI/Mojo-Pg-4.13.tar.gz
+ Mojo-Pg-4.27
+ pathname: S/SR/SRI/Mojo-Pg-4.27.tar.gz
provides:
- Mojo::Pg 4.13
+ Mojo::Pg 4.27
Mojo::Pg::Database undef
Mojo::Pg::Migrations undef
Mojo::Pg::PubSub undef
Mojo::Pg::Results undef
Mojo::Pg::Transaction undef
- SQL::Abstract::Pg undef
requirements:
- DBD::Pg 3.005001
+ DBD::Pg 3.007004
ExtUtils::MakeMaker 0
- Mojolicious 8.03
- SQL::Abstract 1.86
- perl 5.010001
- Mojolicious-8.17
- pathname: S/SR/SRI/Mojolicious-8.17.tar.gz
+ Mojolicious 8.50
+ SQL::Abstract::Pg 1.0
+ perl 5.016
+ Mojolicious-9.36
+ pathname: S/SR/SRI/Mojolicious-9.36.tar.gz
provides:
Mojo undef
Mojo::Asset undef
@@ -1648,7 +1801,6 @@ DISTRIBUTIONS
Mojo::Home undef
Mojo::IOLoop undef
Mojo::IOLoop::Client undef
- Mojo::IOLoop::Delay undef
Mojo::IOLoop::Server undef
Mojo::IOLoop::Stream undef
Mojo::IOLoop::Subprocess undef
@@ -1674,7 +1826,6 @@ DISTRIBUTIONS
Mojo::Server::Morbo::Backend undef
Mojo::Server::Morbo::Backend::Poll undef
Mojo::Server::PSGI undef
- Mojo::Server::PSGI::_IO undef
Mojo::Server::Prefork undef
Mojo::Template undef
Mojo::Transaction undef
@@ -1689,11 +1840,12 @@ DISTRIBUTIONS
Mojo::UserAgent::Transactor undef
Mojo::Util undef
Mojo::WebSocket undef
- Mojolicious 8.17
+ Mojolicious 9.36
Mojolicious::Command undef
Mojolicious::Command::Author::cpanify undef
Mojolicious::Command::Author::generate undef
Mojolicious::Command::Author::generate::app undef
+ Mojolicious::Command::Author::generate::dockerfile undef
Mojolicious::Command::Author::generate::lite_app undef
Mojolicious::Command::Author::generate::makefile undef
Mojolicious::Command::Author::generate::plugin undef
@@ -1717,6 +1869,7 @@ DISTRIBUTIONS
Mojolicious::Plugin::HeaderCondition undef
Mojolicious::Plugin::JSONConfig undef
Mojolicious::Plugin::Mount undef
+ Mojolicious::Plugin::NotYAMLConfig undef
Mojolicious::Plugin::TagHelpers undef
Mojolicious::Plugins undef
Mojolicious::Renderer undef
@@ -1734,49 +1887,52 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
IO::Socket::IP 0.37
- JSON::PP 2.27103
- List::Util 1.41
- Time::Local 1.2
- perl 5.010001
- Mojolicious-Plugin-Authentication-1.33
- pathname: J/JJ/JJATRIA/Mojolicious-Plugin-Authentication-1.33.tar.gz
+ Sub::Util 1.41
+ perl 5.016
+ Mojolicious-Plugin-Authentication-1.39
+ pathname: J/JJ/JJATRIA/Mojolicious-Plugin-Authentication-1.39.tar.gz
provides:
- Mojolicious::Plugin::Authentication 1.33
+ Mojolicious::Plugin::Authentication 1.39
requirements:
+ Exporter 0
ExtUtils::MakeMaker 0
- Mojo::Base 0
- perl 5.010
- strict 0
- warnings 0
- Moo-2.003004
- pathname: H/HA/HAARG/Moo-2.003004.tar.gz
+ Mojolicious 8.0
+ perl 5.016
+ Mojolicious-Plugin-OAuth2-2.02
+ pathname: J/JH/JHTHORSEN/Mojolicious-Plugin-OAuth2-2.02.tar.gz
+ provides:
+ Mojolicious::Plugin::OAuth2 2.02
+ Mojolicious::Plugin::OAuth2::Mock undef
+ requirements:
+ ExtUtils::MakeMaker 0
+ IO::Socket::SSL 1.94
+ Mojolicious 8.25
+ Moo-2.005005
+ pathname: H/HA/HAARG/Moo-2.005005.tar.gz
provides:
Method::Generate::Accessor undef
Method::Generate::BuildAll undef
Method::Generate::Constructor undef
Method::Generate::DemolishAll undef
- Moo 2.003004
+ Moo 2.005005
Moo::HandleMoose undef
Moo::HandleMoose::FakeConstructor undef
Moo::HandleMoose::FakeMetaClass undef
Moo::HandleMoose::_TypeMap undef
Moo::Object undef
- Moo::Role 2.003004
+ Moo::Role 2.005005
Moo::_Utils undef
- Moo::_mro undef
- Moo::_strictures undef
Moo::sification undef
oo undef
requirements:
- Class::Method::Modifiers 1.1
- Devel::GlobalDestruction 0.11
- Exporter 5.57
+ Carp 0
+ Class::Method::Modifiers 1.10
+ Exporter 0
ExtUtils::MakeMaker 0
- Module::Runtime 0.014
- Role::Tiny 2.000004
- Scalar::Util 0
- Sub::Defer 2.003001
- Sub::Quote 2.003001
+ Role::Tiny 2.002003
+ Scalar::Util 1.00
+ Sub::Defer 2.006006
+ Sub::Quote 2.006006
perl 5.006
MooX-Types-MooseLike-0.29
pathname: M/MA/MATEU/MooX-Types-MooseLike-0.29.tar.gz
@@ -1786,13 +1942,19 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Module::Runtime 0.014
- Net-HTTP-6.19
- pathname: O/OA/OALDERS/Net-HTTP-6.19.tar.gz
+ Mozilla-CA-20240313
+ pathname: L/LW/LWP/Mozilla-CA-20240313.tar.gz
+ provides:
+ Mozilla::CA 20240313
+ requirements:
+ ExtUtils::MakeMaker 0
+ Net-HTTP-6.23
+ pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz
provides:
- Net::HTTP 6.19
- Net::HTTP::Methods 6.19
- Net::HTTP::NB 6.19
- Net::HTTPS 6.19
+ Net::HTTP 6.23
+ Net::HTTP::Methods 6.23
+ Net::HTTP::NB 6.23
+ Net::HTTPS 6.23
requirements:
Carp 0
Compress::Raw::Zlib 0
@@ -1803,20 +1965,30 @@ DISTRIBUTIONS
base 0
perl 5.006002
strict 0
- vars 0
warnings 0
- Package-Stash-0.38
- pathname: E/ET/ETHER/Package-Stash-0.38.tar.gz
+ Net-SSLeay-1.94
+ pathname: C/CH/CHRISN/Net-SSLeay-1.94.tar.gz
+ provides:
+ Net::SSLeay 1.94
+ Net::SSLeay::Handle 1.94
+ requirements:
+ English 0
+ ExtUtils::MakeMaker 0
+ File::Spec::Functions 0
+ MIME::Base64 0
+ Text::Wrap 0
+ constant 0
+ perl 5.008001
+ Package-Stash-0.40
+ pathname: E/ET/ETHER/Package-Stash-0.40.tar.gz
provides:
- Package::Stash 0.38
- Package::Stash::PP 0.38
+ Package::Stash 0.40
+ Package::Stash::PP 0.40
requirements:
B 0
Carp 0
- Config 0
Dist::CheckConflicts 0.02
ExtUtils::MakeMaker 0
- File::Spec 0
Getopt::Long 0
Module::Implementation 0.06
Package::Stash::XS 0.26
@@ -1827,10 +1999,10 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- Package-Stash-XS-0.29
- pathname: E/ET/ETHER/Package-Stash-XS-0.29.tar.gz
+ Package-Stash-XS-0.30
+ pathname: E/ET/ETHER/Package-Stash-XS-0.30.tar.gz
provides:
- Package::Stash::XS 0.29
+ Package::Stash::XS 0.30
requirements:
ExtUtils::MakeMaker 0
XSLoader 0
@@ -1850,23 +2022,28 @@ DISTRIBUTIONS
perl 5.006001
strict 0
warnings 0
- Params-Util-1.07
- pathname: A/AD/ADAMK/Params-Util-1.07.tar.gz
+ Params-Util-1.102
+ pathname: R/RE/REHSACK/Params-Util-1.102.tar.gz
provides:
- Params::Util 1.07
+ Params::Util 1.102
+ Params::Util::PP 1.102
requirements:
- ExtUtils::CBuilder 0.27
- ExtUtils::MakeMaker 6.52
- File::Spec 0.80
+ Carp 0
+ ExtUtils::MakeMaker 0
+ File::Basename 0
+ File::Copy 0
+ File::Path 0
+ File::Spec 0
+ IPC::Cmd 0
Scalar::Util 1.18
- Test::More 0.42
- perl 5.00503
- Params-ValidationCompiler-0.30
- pathname: D/DR/DROLSKY/Params-ValidationCompiler-0.30.tar.gz
+ XSLoader 0.22
+ parent 0
+ Params-ValidationCompiler-0.31
+ pathname: D/DR/DROLSKY/Params-ValidationCompiler-0.31.tar.gz
provides:
- Params::ValidationCompiler 0.30
- Params::ValidationCompiler::Compiler 0.30
- Params::ValidationCompiler::Exceptions 0.30
+ Params::ValidationCompiler 0.31
+ Params::ValidationCompiler::Compiler 0.31
+ Params::ValidationCompiler::Exceptions 0.31
requirements:
B 0
Carp 0
@@ -1879,11 +2056,11 @@ DISTRIBUTIONS
overload 0
strict 0
warnings 0
- Path-Tiny-0.108
- pathname: D/DA/DAGOLDEN/Path-Tiny-0.108.tar.gz
+ Path-Tiny-0.144
+ pathname: D/DA/DAGOLDEN/Path-Tiny-0.144.tar.gz
provides:
- Path::Tiny 0.108
- Path::Tiny::Error 0.108
+ Path::Tiny 0.144
+ Path::Tiny::Error 0.144
requirements:
Carp 0
Cwd 0
@@ -1893,6 +2070,7 @@ DISTRIBUTIONS
Exporter 5.57
ExtUtils::MakeMaker 6.17
Fcntl 0
+ File::Compare 0
File::Copy 0
File::Glob 0
File::Path 2.07
@@ -1905,18 +2083,34 @@ DISTRIBUTIONS
strict 0
warnings 0
warnings::register 0
- Role-Tiny-2.000006
- pathname: H/HA/HAARG/Role-Tiny-2.000006.tar.gz
+ PkgConfig-0.25026
+ pathname: P/PL/PLICEASE/PkgConfig-0.25026.tar.gz
provides:
- Role::Tiny 2.000006
- Role::Tiny::With 2.000006
+ PkgConfig 0.25026
+ requirements:
+ ExtUtils::MakeMaker 6.56
+ Test::More 0.94
+ perl 5.006000
+ Role-Tiny-2.002004
+ pathname: H/HA/HAARG/Role-Tiny-2.002004.tar.gz
+ provides:
+ Role::Tiny 2.002004
+ Role::Tiny::With 2.002004
requirements:
Exporter 5.57
perl 5.006
- SQL-Abstract-1.86
- pathname: I/IL/ILMARI/SQL-Abstract-1.86.tar.gz
- provides:
- SQL::Abstract 1.86
+ SQL-Abstract-2.000001
+ pathname: M/MS/MSTROUT/SQL-Abstract-2.000001.tar.gz
+ provides:
+ Chunkstrumenter undef
+ DBIx::Class::SQLMaker::Role::SQLA2Passthrough undef
+ SQL::Abstract 2.000001
+ SQL::Abstract::Formatter undef
+ SQL::Abstract::Parts undef
+ SQL::Abstract::Plugin::BangOverrides undef
+ SQL::Abstract::Plugin::ExtraClauses undef
+ SQL::Abstract::Reference undef
+ SQL::Abstract::Role::Plugin undef
SQL::Abstract::Test undef
SQL::Abstract::Tree undef
requirements:
@@ -1928,61 +2122,61 @@ DISTRIBUTIONS
Moo 2.000001
Scalar::Util 0
Sub::Quote 2.000001
+ Test::Builder::Module 0.84
+ Test::Deep 0.101
Text::Balanced 2.00
perl 5.006
- Sort-Versions-1.62
- pathname: N/NE/NEILB/Sort-Versions-1.62.tar.gz
- provides:
- Sort::Versions 1.62
- requirements:
- Exporter 0
- ExtUtils::MakeMaker 0
- perl 5.006
- strict 0
- warnings 0
- Specio-0.43
- pathname: D/DR/DROLSKY/Specio-0.43.tar.gz
- provides:
- Specio 0.43
- Specio::Coercion 0.43
- Specio::Constraint::AnyCan 0.43
- Specio::Constraint::AnyDoes 0.43
- Specio::Constraint::AnyIsa 0.43
- Specio::Constraint::Enum 0.43
- Specio::Constraint::Intersection 0.43
- Specio::Constraint::ObjectCan 0.43
- Specio::Constraint::ObjectDoes 0.43
- Specio::Constraint::ObjectIsa 0.43
- Specio::Constraint::Parameterizable 0.43
- Specio::Constraint::Parameterized 0.43
- Specio::Constraint::Role::CanType 0.43
- Specio::Constraint::Role::DoesType 0.43
- Specio::Constraint::Role::Interface 0.43
- Specio::Constraint::Role::IsaType 0.43
- Specio::Constraint::Simple 0.43
- Specio::Constraint::Structurable 0.43
- Specio::Constraint::Structured 0.43
- Specio::Constraint::Union 0.43
- Specio::Declare 0.43
- Specio::DeclaredAt 0.43
- Specio::Exception 0.43
- Specio::Exporter 0.43
- Specio::Helpers 0.43
- Specio::Library::Builtins 0.43
- Specio::Library::Numeric 0.43
- Specio::Library::Perl 0.43
- Specio::Library::String 0.43
- Specio::Library::Structured 0.43
- Specio::Library::Structured::Dict 0.43
- Specio::Library::Structured::Map 0.43
- Specio::Library::Structured::Tuple 0.43
- Specio::OO 0.43
- Specio::PartialDump 0.43
- Specio::Registry 0.43
- Specio::Role::Inlinable 0.43
- Specio::Subs 0.43
- Specio::TypeChecks 0.43
- Test::Specio 0.43
+ SQL-Abstract-Pg-1.0
+ pathname: S/SR/SRI/SQL-Abstract-Pg-1.0.tar.gz
+ provides:
+ SQL::Abstract::Pg 1.0
+ requirements:
+ ExtUtils::MakeMaker 0
+ SQL::Abstract 2.0
+ perl 5.016
+ Specio-0.48
+ pathname: D/DR/DROLSKY/Specio-0.48.tar.gz
+ provides:
+ Specio 0.48
+ Specio::Coercion 0.48
+ Specio::Constraint::AnyCan 0.48
+ Specio::Constraint::AnyDoes 0.48
+ Specio::Constraint::AnyIsa 0.48
+ Specio::Constraint::Enum 0.48
+ Specio::Constraint::Intersection 0.48
+ Specio::Constraint::ObjectCan 0.48
+ Specio::Constraint::ObjectDoes 0.48
+ Specio::Constraint::ObjectIsa 0.48
+ Specio::Constraint::Parameterizable 0.48
+ Specio::Constraint::Parameterized 0.48
+ Specio::Constraint::Role::CanType 0.48
+ Specio::Constraint::Role::DoesType 0.48
+ Specio::Constraint::Role::Interface 0.48
+ Specio::Constraint::Role::IsaType 0.48
+ Specio::Constraint::Simple 0.48
+ Specio::Constraint::Structurable 0.48
+ Specio::Constraint::Structured 0.48
+ Specio::Constraint::Union 0.48
+ Specio::Declare 0.48
+ Specio::DeclaredAt 0.48
+ Specio::Exception 0.48
+ Specio::Exporter 0.48
+ Specio::Helpers 0.48
+ Specio::Library::Builtins 0.48
+ Specio::Library::Numeric 0.48
+ Specio::Library::Perl 0.48
+ Specio::Library::String 0.48
+ Specio::Library::Structured 0.48
+ Specio::Library::Structured::Dict 0.48
+ Specio::Library::Structured::Map 0.48
+ Specio::Library::Structured::Tuple 0.48
+ Specio::OO 0.48
+ Specio::PartialDump 0.48
+ Specio::Registry 0.48
+ Specio::Role::Inlinable 0.48
+ Specio::Subs 0.48
+ Specio::TypeChecks 0.48
+ Test::Specio 0.48
requirements:
B 0
Carp 0
@@ -2002,6 +2196,7 @@ DISTRIBUTIONS
Test::Fatal 0
Test::More 0.96
Try::Tiny 0
+ XString 0
overload 0
parent 0
perl 5.008
@@ -2009,17 +2204,18 @@ DISTRIBUTIONS
strict 0
version 0.83
warnings 0
- Sub-Exporter-0.987
- pathname: R/RJ/RJBS/Sub-Exporter-0.987.tar.gz
+ Sub-Exporter-0.991
+ pathname: R/RJ/RJBS/Sub-Exporter-0.991.tar.gz
provides:
- Sub::Exporter 0.987
- Sub::Exporter::Util 0.987
+ Sub::Exporter 0.991
+ Sub::Exporter::Util 0.991
requirements:
Carp 0
Data::OptList 0.100
- ExtUtils::MakeMaker 6.30
+ ExtUtils::MakeMaker 6.78
Params::Util 0.14
Sub::Install 0.92
+ perl 5.012
strict 0
warnings 0
Sub-Exporter-Progressive-0.001013
@@ -2035,44 +2231,144 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Test::More 0
- Sub-Install-0.928
- pathname: R/RJ/RJBS/Sub-Install-0.928.tar.gz
+ Sub-Install-0.929
+ pathname: R/RJ/RJBS/Sub-Install-0.929.tar.gz
provides:
- Sub::Install 0.928
+ Sub::Install 0.929
requirements:
B 0
Carp 0
- ExtUtils::MakeMaker 6.30
+ ExtUtils::MakeMaker 6.78
Scalar::Util 0
+ perl 5.008000
strict 0
warnings 0
- Sub-Quote-2.006003
- pathname: H/HA/HAARG/Sub-Quote-2.006003.tar.gz
+ Sub-Quote-2.006008
+ pathname: H/HA/HAARG/Sub-Quote-2.006008.tar.gz
provides:
- Sub::Defer 2.006003
- Sub::Quote 2.006003
+ Sub::Defer 2.006008
+ Sub::Quote 2.006008
requirements:
ExtUtils::MakeMaker 0
Scalar::Util 0
perl 5.006
- Test-Compile-v1.3.0
- pathname: E/EG/EGILES/Test-Compile-v1.3.0.tar.gz
+ Sub-Uplevel-0.2800
+ pathname: D/DA/DAGOLDEN/Sub-Uplevel-0.2800.tar.gz
+ provides:
+ Sub::Uplevel 0.2800
+ requirements:
+ Carp 0
+ ExtUtils::MakeMaker 6.17
+ constant 0
+ perl 5.006
+ strict 0
+ warnings 0
+ Test-Compile-v3.3.1
+ pathname: E/EG/EGILES/Test-Compile-v3.3.1.tar.gz
provides:
- Test::Compile v1.3.0
- Test::Compile::Internal v1.3.0
+ Test::Compile v3.3.1
+ Test::Compile::Internal v3.3.1
requirements:
+ Exporter 5.68
Module::Build 0.38
- UNIVERSAL::require 0
- perl v5.6.2
- version 0
- Test-Fatal-0.014
- pathname: R/RJ/RJBS/Test-Fatal-0.014.tar.gz
+ parent 0.225
+ perl v5.10.0
+ version 0.77
+ Test-Deep-1.204
+ pathname: R/RJ/RJBS/Test-Deep-1.204.tar.gz
+ provides:
+ Test::Deep 1.204
+ Test::Deep::All 1.204
+ Test::Deep::Any 1.204
+ Test::Deep::Array 1.204
+ Test::Deep::ArrayEach 1.204
+ Test::Deep::ArrayElementsOnly 1.204
+ Test::Deep::ArrayLength 1.204
+ Test::Deep::ArrayLengthOnly 1.204
+ Test::Deep::Blessed 1.204
+ Test::Deep::Boolean 1.204
+ Test::Deep::Cache 1.204
+ Test::Deep::Cache::Simple 1.204
+ Test::Deep::Class 1.204
+ Test::Deep::Cmp 1.204
+ Test::Deep::Code 1.204
+ Test::Deep::Hash 1.204
+ Test::Deep::HashEach 1.204
+ Test::Deep::HashElements 1.204
+ Test::Deep::HashKeys 1.204
+ Test::Deep::HashKeysOnly 1.204
+ Test::Deep::Ignore 1.204
+ Test::Deep::Isa 1.204
+ Test::Deep::ListMethods 1.204
+ Test::Deep::MM 1.204
+ Test::Deep::Methods 1.204
+ Test::Deep::NoTest 1.204
+ Test::Deep::None 1.204
+ Test::Deep::Number 1.204
+ Test::Deep::Obj 1.204
+ Test::Deep::Ref 1.204
+ Test::Deep::RefType 1.204
+ Test::Deep::Regexp 1.204
+ Test::Deep::RegexpMatches 1.204
+ Test::Deep::RegexpOnly 1.204
+ Test::Deep::RegexpRef 1.204
+ Test::Deep::RegexpRefOnly 1.204
+ Test::Deep::RegexpVersion 1.204
+ Test::Deep::ScalarRef 1.204
+ Test::Deep::ScalarRefOnly 1.204
+ Test::Deep::Set 1.204
+ Test::Deep::Shallow 1.204
+ Test::Deep::Stack 1.204
+ Test::Deep::String 1.204
+ Test::Deep::SubHash 1.204
+ Test::Deep::SubHashElements 1.204
+ Test::Deep::SubHashKeys 1.204
+ Test::Deep::SubHashKeysOnly 1.204
+ Test::Deep::SuperHash 1.204
+ Test::Deep::SuperHashElements 1.204
+ Test::Deep::SuperHashKeys 1.204
+ Test::Deep::SuperHashKeysOnly 1.204
+ requirements:
+ ExtUtils::MakeMaker 6.78
+ List::Util 1.09
+ Scalar::Util 1.09
+ Test::Builder 0
+ Test::More 0.96
+ perl 5.012
+ Test-Differences-0.71
+ pathname: D/DC/DCANTRELL/Test-Differences-0.71.tar.gz
provides:
- Test::Fatal 0.014
+ Test::Differences 0.71
+ requirements:
+ Capture::Tiny 0.24
+ Data::Dumper 2.126
+ ExtUtils::MakeMaker 0
+ Test::More 0.88
+ Text::Diff 1.43
+ Test-Exception-0.43
+ pathname: E/EX/EXODIST/Test-Exception-0.43.tar.gz
+ provides:
+ Test::Exception 0.43
requirements:
Carp 0
- Exporter 5.57
+ Exporter 0
ExtUtils::MakeMaker 0
+ Sub::Uplevel 0.18
+ Test::Builder 0.7
+ Test::Builder::Tester 1.07
+ Test::Harness 2.03
+ base 0
+ perl 5.006001
+ strict 0
+ warnings 0
+ Test-Fatal-0.017
+ pathname: R/RJ/RJBS/Test-Fatal-0.017.tar.gz
+ provides:
+ Test::Fatal 0.017
+ requirements:
+ Carp 0
+ Exporter 5.57
+ ExtUtils::MakeMaker 6.78
Test::Builder 0
Try::Tiny 0.07
strict 0
@@ -2101,18 +2397,30 @@ DISTRIBUTIONS
Test::Builder::Tester 1.02
Test::More 0.62
perl 5.008
- Text-CSV-2.00
- pathname: I/IS/ISHIGAKI/Text-CSV-2.00.tar.gz
+ Text-CSV-2.04
+ pathname: I/IS/ISHIGAKI/Text-CSV-2.04.tar.gz
provides:
- Text::CSV 2.00
- Text::CSV::ErrorDiag 2.00
- Text::CSV_PP 2.00
+ Text::CSV 2.04
+ Text::CSV::ErrorDiag 2.04
+ Text::CSV_PP 2.04
requirements:
ExtUtils::MakeMaker 0
IO::Handle 0
Test::Harness 0
- Test::More 0.71
+ Test::More 0.92
perl 5.006001
+ Text-Diff-1.45
+ pathname: N/NE/NEILB/Text-Diff-1.45.tar.gz
+ provides:
+ Text::Diff 1.45
+ Text::Diff::Base 1.45
+ Text::Diff::Config 1.44
+ Text::Diff::Table 1.44
+ requirements:
+ Algorithm::Diff 1.19
+ Exporter 0
+ ExtUtils::MakeMaker 0
+ perl 5.006
Text-LevenshteinXS-0.03
pathname: J/JG/JGOLDBERG/Text-LevenshteinXS-0.03.tar.gz
provides:
@@ -2120,24 +2428,67 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Test 0
- Throwable-0.200013
- pathname: R/RJ/RJBS/Throwable-0.200013.tar.gz
+ Text-Markdown-1.000031
+ pathname: B/BO/BOBTFISH/Text-Markdown-1.000031.tar.gz
provides:
- StackTrace::Auto 0.200013
- Throwable 0.200013
- Throwable::Error 0.200013
+ Text::Markdown 1.000031
+ requirements:
+ Digest::MD5 0
+ Encode 0
+ ExtUtils::MakeMaker 6.42
+ FindBin 0
+ List::MoreUtils 0
+ Test::Differences 0
+ Test::Exception 0
+ Test::More 0.42
+ Text::Balanced 0
+ Text-PDF-0.31
+ pathname: B/BH/BHALLISSY/Text-PDF-0.31.tar.gz
+ provides:
+ Text::PDF 0.31
+ Text::PDF::ASCII85Decode undef
+ Text::PDF::ASCIIHexDecode undef
+ Text::PDF::Array undef
+ Text::PDF::Bool undef
+ Text::PDF::Dict undef
+ Text::PDF::File undef
+ Text::PDF::Filter undef
+ Text::PDF::FlateDecode undef
+ Text::PDF::LZWDecode undef
+ Text::PDF::Name undef
+ Text::PDF::Null undef
+ Text::PDF::Number undef
+ Text::PDF::Objind undef
+ Text::PDF::Page undef
+ Text::PDF::Pages undef
+ Text::PDF::RunLengthDecode undef
+ Text::PDF::SFont undef
+ Text::PDF::String undef
+ Text::PDF::TTFont undef
+ Text::PDF::TTFont0 undef
+ Text::PDF::TTIOString undef
+ Text::PDF::Utils undef
+ requirements:
+ Compress::Zlib 0
+ ExtUtils::MakeMaker 0
+ Throwable-1.001
+ pathname: R/RJ/RJBS/Throwable-1.001.tar.gz
+ provides:
+ StackTrace::Auto 1.001
+ Throwable 1.001
+ Throwable::Error 1.001
requirements:
Carp 0
Devel::StackTrace 1.32
- ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker 6.78
Module::Runtime 0.002
Moo 1.000001
Moo::Role 0
Scalar::Util 0
Sub::Quote 0
overload 0
- TimeDate-2.30
- pathname: G/GB/GBARR/TimeDate-2.30.tar.gz
+ TimeDate-2.33
+ pathname: A/AT/ATOOMIC/TimeDate-2.33.tar.gz
provides:
Date::Format 2.24
Date::Format::Generic 2.24
@@ -2162,6 +2513,7 @@ DISTRIBUTIONS
Date::Language::Icelandic 1.01
Date::Language::Italian 1.01
Date::Language::Norwegian 1.01
+ Date::Language::Occitan 1.04
Date::Language::Oromo 0.99
Date::Language::Romanian 1.01
Date::Language::Russian 1.01
@@ -2175,24 +2527,76 @@ DISTRIBUTIONS
Date::Language::TigrinyaEritrean 1.00
Date::Language::TigrinyaEthiopian 1.00
Date::Language::Turkish 1.0
- Date::Parse 2.30
+ Date::Parse 2.33
Time::Zone 2.24
+ TimeDate 1.21
requirements:
ExtUtils::MakeMaker 0
- Travel-Status-DE-IRIS-1.26
- pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.26.tar.gz
+ Travel-Status-DE-DBWagenreihung-0.12
+ pathname: D/DE/DERF/Travel-Status-DE-DBWagenreihung-0.12.tar.gz
+ provides:
+ Travel::Status::DE::DBWagenreihung 0.12
+ Travel::Status::DE::DBWagenreihung::Section 0.12
+ Travel::Status::DE::DBWagenreihung::Wagon 0.12
+ requirements:
+ Carp 0
+ Class::Accessor 0
+ Getopt::Long 0
+ JSON 0
+ LWP::UserAgent 0
+ List::Util 0
+ Module::Build 0.4
+ Test::Compile 0
+ Test::More 0
+ Test::Pod 0
+ Travel::Status::DE::IRIS 1.2
+ perl v5.20.0
+ Travel-Status-DE-DeutscheBahn-6.03
+ pathname: D/DE/DERF/Travel-Status-DE-DeutscheBahn-6.03.tar.gz
+ provides:
+ Travel::Status::DE::DeutscheBahn 6.03
+ Travel::Status::DE::HAFAS 6.03
+ Travel::Status::DE::HAFAS::Journey 6.03
+ Travel::Status::DE::HAFAS::Location 6.03
+ Travel::Status::DE::HAFAS::Message 6.03
+ Travel::Status::DE::HAFAS::Polyline 6.03
+ Travel::Status::DE::HAFAS::Product 6.03
+ Travel::Status::DE::HAFAS::Stop 6.03
+ Travel::Status::DE::HAFAS::StopFinder 6.03
+ requirements:
+ Carp 0
+ Class::Accessor 0.16
+ DateTime 0
+ DateTime::Format::Strptime 0
+ Digest::MD5 0
+ Getopt::Long 0
+ JSON 0
+ LWP::Protocol::https 0
+ LWP::UserAgent 0
+ List::MoreUtils 0
+ List::Util 0
+ Module::Build 0.4
+ Test::Compile 0
+ Test::More 0
+ Test::Pod 0
+ perl v5.14.0
+ Travel-Status-DE-IRIS-1.96
+ pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.96.tar.gz
provides:
- Travel::Status::DE::IRIS 1.26
- Travel::Status::DE::IRIS::Result 1.26
- Travel::Status::DE::IRIS::Stations 1.26
+ Travel::Status::DE::IRIS 1.96
+ Travel::Status::DE::IRIS::Result 1.96
+ Travel::Status::DE::IRIS::Stations 1.96
requirements:
Carp 0
Class::Accessor 0
DateTime 0
DateTime::Format::Strptime 0
Encode 0
- Geo::Distance 0
+ File::Slurp 9999.19
+ GIS::Distance 0
Getopt::Long 0
+ JSON 0
+ LWP::Protocol::https 0
LWP::UserAgent 0
List::Compare 0.29
List::MoreUtils 0
@@ -2208,10 +2612,10 @@ DISTRIBUTIONS
Text::LevenshteinXS 0
XML::LibXML 0
perl v5.14.2
- Try-Tiny-0.30
- pathname: E/ET/ETHER/Try-Tiny-0.30.tar.gz
+ Try-Tiny-0.31
+ pathname: E/ET/ETHER/Try-Tiny-0.31.tar.gz
provides:
- Try::Tiny 0.30
+ Try::Tiny 0.31
requirements:
Carp 0
Exporter 5.57
@@ -2220,73 +2624,66 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- Types-Serialiser-1.0
- pathname: M/ML/MLEHMANN/Types-Serialiser-1.0.tar.gz
+ Types-Serialiser-1.01
+ pathname: M/ML/MLEHMANN/Types-Serialiser-1.01.tar.gz
provides:
- JSON::PP::Boolean 1.0
- Types::Serialiser 1.0
- Types::Serialiser::BooleanBase 1.0
- Types::Serialiser::Error 1.0
+ JSON::PP::Boolean 1.01
+ Types::Serialiser 1.01
+ Types::Serialiser::BooleanBase 1.01
+ Types::Serialiser::Error 1.01
requirements:
ExtUtils::MakeMaker 0
common::sense 0
- UNIVERSAL-require-0.18
- pathname: N/NE/NEILB/UNIVERSAL-require-0.18.tar.gz
- provides:
- UNIVERSAL::require 0.18
- requirements:
- Carp 0
- ExtUtils::MakeMaker 0
- Test::More 0.47
- perl 5.006
- strict 0
- warnings 0
- URI-1.76
- pathname: O/OA/OALDERS/URI-1.76.tar.gz
- provides:
- URI 1.76
- URI::Escape 3.31
- URI::Heuristic 4.20
- URI::IRI 1.76
- URI::QueryParam 1.76
- URI::Split 1.76
- URI::URL 5.04
- URI::WithBase 2.20
- URI::data 1.76
- URI::file 4.21
- URI::file::Base 1.76
- URI::file::FAT 1.76
- URI::file::Mac 1.76
- URI::file::OS2 1.76
- URI::file::QNX 1.76
- URI::file::Unix 1.76
- URI::file::Win32 1.76
- URI::ftp 1.76
- URI::gopher 1.76
- URI::http 1.76
- URI::https 1.76
- URI::ldap 1.76
- URI::ldapi 1.76
- URI::ldaps 1.76
- URI::mailto 1.76
- URI::mms 1.76
- URI::news 1.76
- URI::nntp 1.76
- URI::pop 1.76
- URI::rlogin 1.76
- URI::rsync 1.76
- URI::rtsp 1.76
- URI::rtspu 1.76
- URI::sftp 1.76
- URI::sip 1.76
- URI::sips 1.76
- URI::snews 1.76
- URI::ssh 1.76
- URI::telnet 1.76
- URI::tn3270 1.76
- URI::urn 1.76
- URI::urn::isbn 1.76
- URI::urn::oid 1.76
+ URI-5.28
+ pathname: O/OA/OALDERS/URI-5.28.tar.gz
+ provides:
+ URI 5.28
+ URI::Escape 5.28
+ URI::Heuristic 5.28
+ URI::IRI 5.28
+ URI::QueryParam 5.28
+ URI::Split 5.28
+ URI::URL 5.28
+ URI::WithBase 5.28
+ URI::data 5.28
+ URI::file 5.28
+ URI::file::Base 5.28
+ URI::file::FAT 5.28
+ URI::file::Mac 5.28
+ URI::file::OS2 5.28
+ URI::file::QNX 5.28
+ URI::file::Unix 5.28
+ URI::file::Win32 5.28
+ URI::ftp 5.28
+ URI::geo 5.28
+ URI::gopher 5.28
+ URI::http 5.28
+ URI::https 5.28
+ URI::icap 5.28
+ URI::icaps 5.28
+ URI::ldap 5.28
+ URI::ldapi 5.28
+ URI::ldaps 5.28
+ URI::mailto 5.28
+ URI::mms 5.28
+ URI::news 5.28
+ URI::nntp 5.28
+ URI::nntps 5.28
+ URI::pop 5.28
+ URI::rlogin 5.28
+ URI::rsync 5.28
+ URI::rtsp 5.28
+ URI::rtspu 5.28
+ URI::sftp 5.28
+ URI::sip 5.28
+ URI::sips 5.28
+ URI::snews 5.28
+ URI::ssh 5.28
+ URI::telnet 5.28
+ URI::tn3270 5.28
+ URI::urn 5.28
+ URI::urn::isbn 5.28
+ URI::urn::oid 5.28
requirements:
Carp 0
Cwd 0
@@ -2318,10 +2715,10 @@ DISTRIBUTIONS
POSIX 0
Test::More 0
Time::HiRes 0
- Variable-Magic-0.62
- pathname: V/VP/VPIT/Variable-Magic-0.62.tar.gz
+ Variable-Magic-0.64
+ pathname: V/VP/VPIT/Variable-Magic-0.64.tar.gz
provides:
- Variable::Magic 0.62
+ Variable::Magic 0.64
requirements:
Carp 0
Config 0
@@ -2349,47 +2746,48 @@ DISTRIBUTIONS
Fcntl 0
URI 1.10
perl 5.008001
- XML-LibXML-2.0201
- pathname: S/SH/SHLOMIF/XML-LibXML-2.0201.tar.gz
- provides:
- XML::LibXML 2.0201
- XML::LibXML::Attr 2.0201
- XML::LibXML::AttributeHash 2.0201
- XML::LibXML::Boolean 2.0201
- XML::LibXML::CDATASection 2.0201
- XML::LibXML::Comment 2.0201
- XML::LibXML::Common 2.0201
- XML::LibXML::Devel 2.0201
- XML::LibXML::Document 2.0201
- XML::LibXML::DocumentFragment 2.0201
- XML::LibXML::Dtd 2.0201
- XML::LibXML::Element 2.0201
- XML::LibXML::ErrNo 2.0201
- XML::LibXML::Error 2.0201
- XML::LibXML::InputCallback 2.0201
- XML::LibXML::Literal 2.0201
- XML::LibXML::NamedNodeMap 2.0201
- XML::LibXML::Namespace 2.0201
- XML::LibXML::Node 2.0201
- XML::LibXML::NodeList 2.0201
- XML::LibXML::Number 2.0201
- XML::LibXML::PI 2.0201
- XML::LibXML::Pattern 2.0201
- XML::LibXML::Reader 2.0201
- XML::LibXML::RegExp 2.0201
- XML::LibXML::RelaxNG 2.0201
- XML::LibXML::SAX 2.0201
- XML::LibXML::SAX::AttributeNode 2.0201
- XML::LibXML::SAX::Builder 2.0201
- XML::LibXML::SAX::Generator 2.0201
- XML::LibXML::SAX::Parser 2.0201
- XML::LibXML::Schema 2.0201
- XML::LibXML::Text 2.0201
- XML::LibXML::XPathContext 2.0201
- XML::LibXML::XPathExpression 2.0201
- XML::LibXML::_SAXParser 2.0201
- requirements:
- Alien::Libxml2 0
+ XML-LibXML-2.0210
+ pathname: S/SH/SHLOMIF/XML-LibXML-2.0210.tar.gz
+ provides:
+ XML::LibXML 2.0210
+ XML::LibXML::Attr 2.0210
+ XML::LibXML::AttributeHash 2.0210
+ XML::LibXML::Boolean 2.0210
+ XML::LibXML::CDATASection 2.0210
+ XML::LibXML::Comment 2.0210
+ XML::LibXML::Common 2.0210
+ XML::LibXML::Devel 2.0210
+ XML::LibXML::Document 2.0210
+ XML::LibXML::DocumentFragment 2.0210
+ XML::LibXML::Dtd 2.0210
+ XML::LibXML::Element 2.0210
+ XML::LibXML::ErrNo 2.0210
+ XML::LibXML::Error 2.0210
+ XML::LibXML::InputCallback 2.0210
+ XML::LibXML::Literal 2.0210
+ XML::LibXML::NamedNodeMap 2.0210
+ XML::LibXML::Namespace 2.0210
+ XML::LibXML::Node 2.0210
+ XML::LibXML::NodeList 2.0210
+ XML::LibXML::Number 2.0210
+ XML::LibXML::PI 2.0210
+ XML::LibXML::Pattern 2.0210
+ XML::LibXML::Reader 2.0210
+ XML::LibXML::RegExp 2.0210
+ XML::LibXML::RelaxNG 2.0210
+ XML::LibXML::SAX 2.0210
+ XML::LibXML::SAX::AttributeNode 2.0210
+ XML::LibXML::SAX::Builder 2.0210
+ XML::LibXML::SAX::Generator 2.0210
+ XML::LibXML::SAX::Parser 2.0210
+ XML::LibXML::Schema 2.0210
+ XML::LibXML::Text 2.0210
+ XML::LibXML::XPathContext 2.0210
+ XML::LibXML::XPathExpression 2.0210
+ XML::LibXML::_SAXParser 2.0210
+ requirements:
+ Alien::Base::Wrapper 0
+ Alien::Libxml2 0.14
Carp 0
Config 0
DynaLoader 0
@@ -2423,13 +2821,13 @@ DISTRIBUTIONS
strict 0
vars 0
warnings 0
- XML-SAX-1.00
- pathname: G/GR/GRANTM/XML-SAX-1.00.tar.gz
+ XML-SAX-1.02
+ pathname: G/GR/GRANTM/XML-SAX-1.02.tar.gz
provides:
- XML::SAX 1.00
+ XML::SAX 1.02
XML::SAX::DocumentLocator undef
- XML::SAX::ParserFactory 1.01
- XML::SAX::PurePerl 1.00
+ XML::SAX::ParserFactory 1.02
+ XML::SAX::PurePerl 1.02
XML::SAX::PurePerl::DebugHandler undef
XML::SAX::PurePerl::Exception undef
XML::SAX::PurePerl::Productions undef
@@ -2451,6 +2849,13 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
perl 5.008
+ XString-0.005
+ pathname: A/AT/ATOOMIC/XString-0.005.tar.gz
+ provides:
+ XString 0.005
+ requirements:
+ ExtUtils::MakeMaker 0
+ perl 5.008
bareword-filehandles-0.007
pathname: I/IL/ILMARI/bareword-filehandles-0.007.tar.gz
provides:
@@ -2465,16 +2870,16 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- common-sense-3.74
- pathname: M/ML/MLEHMANN/common-sense-3.74.tar.gz
+ common-sense-3.75
+ pathname: M/ML/MLEHMANN/common-sense-3.75.tar.gz
provides:
- common::sense 3.74
+ common::sense 3.75
requirements:
ExtUtils::MakeMaker 0
- indirect-0.38
- pathname: V/VP/VPIT/indirect-0.38.tar.gz
+ indirect-0.39
+ pathname: V/VP/VPIT/indirect-0.39.tar.gz
provides:
- indirect 0.38
+ indirect 0.39
requirements:
Carp 0
Config 0
@@ -2489,57 +2894,55 @@ DISTRIBUTIONS
XSLoader 0
lib 0
perl 5.008001
- libwww-perl-6.39
- pathname: O/OA/OALDERS/libwww-perl-6.39.tar.gz
- provides:
- LWP 6.39
- LWP::Authen::Basic 6.39
- LWP::Authen::Digest 6.39
- LWP::Authen::Ntlm 6.39
- LWP::ConnCache 6.39
- LWP::Debug 6.39
- LWP::Debug::TraceHTTP 6.39
- LWP::DebugFile 6.39
- LWP::MemberMixin 6.39
- LWP::Protocol 6.39
- LWP::Protocol::cpan 6.39
- LWP::Protocol::data 6.39
- LWP::Protocol::file 6.39
- LWP::Protocol::ftp 6.39
- LWP::Protocol::gopher 6.39
- LWP::Protocol::http 6.39
- LWP::Protocol::loopback 6.39
- LWP::Protocol::mailto 6.39
- LWP::Protocol::nntp 6.39
- LWP::Protocol::nogo 6.39
- LWP::RobotUA 6.39
- LWP::Simple 6.39
- LWP::UserAgent 6.39
- libwww::perl undef
- requirements:
- CPAN::Meta::Requirements 2.120620
+ libwww-perl-6.77
+ pathname: O/OA/OALDERS/libwww-perl-6.77.tar.gz
+ provides:
+ LWP 6.77
+ LWP::Authen::Basic 6.77
+ LWP::Authen::Digest 6.77
+ LWP::Authen::Ntlm 6.77
+ LWP::ConnCache 6.77
+ LWP::Debug 6.77
+ LWP::Debug::TraceHTTP 6.77
+ LWP::DebugFile 6.77
+ LWP::MemberMixin 6.77
+ LWP::Protocol 6.77
+ LWP::Protocol::cpan 6.77
+ LWP::Protocol::data 6.77
+ LWP::Protocol::file 6.77
+ LWP::Protocol::ftp 6.77
+ LWP::Protocol::gopher 6.77
+ LWP::Protocol::http 6.77
+ LWP::Protocol::loopback 6.77
+ LWP::Protocol::mailto 6.77
+ LWP::Protocol::nntp 6.77
+ LWP::Protocol::nogo 6.77
+ LWP::RobotUA 6.77
+ LWP::Simple 6.77
+ LWP::UserAgent 6.77
+ requirements:
Digest::MD5 0
Encode 2.12
Encode::Locale 0
ExtUtils::MakeMaker 0
File::Copy 0
File::Listing 6
+ File::Temp 0
Getopt::Long 0
HTML::Entities 0
- HTML::HeadParser 0
+ HTML::HeadParser 3.71
HTTP::Cookies 6
- HTTP::Daemon 6
HTTP::Date 6
HTTP::Negotiate 6
- HTTP::Request 6
- HTTP::Request::Common 6
- HTTP::Response 6
+ HTTP::Request 6.18
+ HTTP::Request::Common 6.18
+ HTTP::Response 6.18
HTTP::Status 6.18
IO::Select 0
IO::Socket 0
LWP::MediaTypes 6
MIME::Base64 2.1
- Module::Metadata 0
+ Module::Load 0
Net::FTP 2.58
Net::HTTP 6.18
Scalar::Util 0
@@ -2547,7 +2950,7 @@ DISTRIBUTIONS
URI 1.10
URI::Escape 0
WWW::RobotRules 6
- base 0
+ parent 0.217
perl 5.008001
strict 0
warnings 0
@@ -2566,10 +2969,10 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- namespace-autoclean-0.28
- pathname: E/ET/ETHER/namespace-autoclean-0.28.tar.gz
+ namespace-autoclean-0.29
+ pathname: E/ET/ETHER/namespace-autoclean-0.29.tar.gz
provides:
- namespace::autoclean 0.28
+ namespace::autoclean 0.29
requirements:
B::Hooks::EndOfScope 0.12
ExtUtils::MakeMaker 0
diff --git a/docker-compose.yml b/docker-compose.yml
index 0b7336d..2aaab4e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,3 +1,7 @@
+# Copyright (C) Markus Witt
+#
+# SPDX-License-Identifier: CC0-1.0
+
version: "3.6"
x-common-env: &common-env
TRAVELYNX_DB_HOST: database
@@ -37,4 +41,4 @@ services:
CRON: 1
networks:
- backend: \ No newline at end of file
+ backend:
diff --git a/docker-run.sh b/docker-run.sh
index 696aa35..14e1405 100755
--- a/docker-run.sh
+++ b/docker-run.sh
@@ -1,58 +1,22 @@
-#!/bin/bash
-set -eu
-
-WAIT_DB_HOST=${TRAVELYNX_DB_HOST}
-WAIT_DB_PORT=5432
-
-check_config() {
- if [ ! -f travelynx.conf ]
- then
- echo "The configuration file is missing"
- exit 1
- fi
-}
-
-wait_for_db() {
- set +e
- for i in $(seq 1 ${WAIT_DB_TIMEOUT:-5})
- do
- (echo >/dev/tcp/${WAIT_DB_HOST}/${WAIT_DB_PORT}) &>/dev/null
- if [ $? -eq 0 ]; then
- break
- else
- echo "Can't reach DB @ ${WAIT_DB_HOST}:${WAIT_DB_PORT}"
- fi
- sleep 1
- done
- set -e
-}
-
-run_app() {
- if [ \
- "${TRAVELYNX_MAIL_DISABLE:-0}" -eq 0 \
- -a "${TRAVELYNX_MAIL_HOST:-unset}" != "unset" \
- ]
- then
- export EMAIL_SENDER_TRANSPORT=SMTP
- export EMAIL_SENDER_TRANSPORT_HOST=${TRAVELYNX_MAIL_HOST}
- export EMAIL_SENDER_TRANSPORT_PORT=${TRAVELYNX_MAIL_PORT:-25}
- fi
-
- perl index.pl database migrate
+#!/bin/sh
+#
+# Copyright (C) Markus Witt
+# Copyright (C) Birte Kristina Friesel
+#
+# SPDX-License-Identifier: CC0-1.0
+
+set -e
+
+if ! [ -r travelynx.conf ]; then
+ echo "Configuration file (travelynx.conf) is missing. Did you set up the '/local' mountpoint?"
+ exit 1
+fi
- exec /usr/local/bin/hypnotoad -f index.pl
-}
+. local/email-transport.sh
-run_cron() {
+if [ "$1" = worker ]; then
exec perl index.pl worker
-}
-
-check_config
-wait_for_db
-
-if [ "${CRON:-0}" -ne "0" ]
-then
- run_cron
fi
-run_app \ No newline at end of file
+perl index.pl database migrate
+exec hypnotoad -f index.pl
diff --git a/examples/docker/email-transport.sh b/examples/docker/email-transport.sh
new file mode 100644
index 0000000..c04f187
--- /dev/null
+++ b/examples/docker/email-transport.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+export EMAIL_SENDER_TRANSPORT=SMTP
+export EMAIL_SENDER_TRANSPORT_HOST=smtp.example.com
+export EMAIL_SENDER_TRANSPORT_PORT=25
diff --git a/examples/docker/travelynx.conf b/examples/docker/travelynx.conf
deleted file mode 100644
index b3dc003..0000000
--- a/examples/docker/travelynx.conf
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- cache => {
- schedule => $ENV{TRAVELYNX_IRIS_CACHE} // '/var/cache/travelynx/iris',
- realtime => $ENV{TRAVELYNX_IRISRT_CACHE} // '/var/cache/travelynx/iris-rt',
- },
- db => {
- host => $ENV{TRAVELYNX_DB_HOST} // die("Please set TRAVELYNX_DB_HOST"),
- database => $ENV{TRAVELYNX_DB_NAME} // 'travelynx',
- user => $ENV{TRAVELYNX_DB_USERNAME} // 'travelynx',
- password => $ENV{TRAVELYNX_DB_PASSWORD} // die("Please set TRAVELYNX_DB_PASSWORD"),
- },
- hypnotoad => {
- accepts => $ENV{TRAVELYNX_HYPNOTOAD_ACCEPTS} // 100,
- clients => $ENV{TRAVELYNX_HYPNOTOAD_CLIENTS} // 10,
- listen => [ $ENV{TRALELYNX_HYPNOTOAD_LISTEN} // 'http://*:8093' ],
- pid_file => '/tmp/travelynx.pid',
- workers => $ENV{TRAVELYNX_HYPNOTOAD_WORKERS} // 2,
- spare => $ENV{TRAVELYNX_HYPNOTOAD_SPARE} // 2,
- },
- mail => {
- disabled => $ENV{TRAVELYNX_MAIL_DISABLE} // 0,
- },
- secrets => [
- $ENV{TRAVELYNX_SECRET} // die("Please set TRAVELYNX_SECRET"),
- ],
-};
diff --git a/examples/nginx/travelynx.conf b/examples/nginx/travelynx.conf
new file mode 100644
index 0000000..da8426e
--- /dev/null
+++ b/examples/nginx/travelynx.conf
@@ -0,0 +1,40 @@
+upstream travelynx {
+ server 127.0.0.1:8093;
+}
+
+server {
+ listen [::]:80;
+ listen 0.0.0.0:80;
+
+ server_name travelynx.de;
+ access_log /var/log/nginx/de.travelynx.log anonip;
+
+ rewrite ^/(.*)$ https://travelynx.de/$1 permanent;
+}
+
+
+server {
+ listen [::]:443;
+ listen 0.0.0.0:443;
+
+ server_name travelynx.de;
+ access_log /var/log/nginx/de.travelynx.log anonip;
+
+ location /static {
+ expires 1y;
+ root /srv/www/travelynx/public;
+ }
+
+ location /service-worker.js {
+ expires 1d;
+ root /srv/www/travelynx/public;
+ }
+
+ location / {
+ proxy_pass http://travelynx;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto "http";
+ }
+}
diff --git a/examples/travelynx.conf b/examples/travelynx.conf
index fe3e960..f8eaac0 100644
--- a/examples/travelynx.conf
+++ b/examples/travelynx.conf
@@ -2,9 +2,22 @@
# travelynx.conf must be a valid perl hash reference. String values must be
# quoted and hash items must end with a comma. You can access environment
# variables via $ENV, e.g. by writing $ENV{TRAVELYNX_DB_HOST} instead of
-# 'localhost'.
+# 'localhost'. You can validate via 'perl -c travelynx.conf'.
{
+ # Optional announcement, e.g. to indicate maintenance or backend issues.
+ #announcement => 'The IRIS backend is flaky. Real-time data may not be available.',
+
+ # Base URL of this travelynx installation, e.g. "https://travelynx.de" for
+ # travelynx.de. Used to identify this travelynx instance when performing API
+ # requests (so API providers know whom to contact case of issues) and for
+ # imprint and other links in travelynx E-Mails. Note that this entry is
+ # only used when travelynx is performing requests or sending E-mails from
+ # a "work", "worker", or "maintenance" job. Otherwise, it will infer the
+ # base URL from the HTTP request. If your travelynx instance is reachable
+ # via multiple URLs, use any one of them.
+ base_url => Mojo::URL->new('https://FIXME.local'),
+
# Cache directories for schedule and realtime data. Mandatory. The parent
# directory ('/var/cache/travelynx' in this case) must already exist.
cache => {
@@ -22,16 +35,11 @@
password => die("Changeme!"),
},
- mail => {
- # If you want to disable outgoing mail for development purposes,
- # uncomment the following line. Mails will instead be logged as
- # Mojolicious "info" messages, causing their content to be printed on
- # stdout.
- ## disabled => 1,
- },
-
- # See the Mojo::Server::Hypnotoad manual for details on the following
- # settings.
+ # These settings control the amount and (re)spawn behaviour of travelynx
+ # worker processes as well as IP, port, and PID file. They are suitable for
+ # up to a few dozen concurrent users. If your site has more traffic, you
+ # may want to increase the number of worker processes.
+ # See the Mojo::Server::Hypnotoad manual for details.
hypnotoad => {
accepts => 100,
clients => 10,
@@ -41,6 +49,46 @@
spare => 2,
},
+ influxdb => {
+ # travelynx can log statistics and performance attributes to InfluxDB.
+ # To do so, create a travelynx database in your InfluxDB, and point url
+ # (below) to the corresponding write URL. The URL may use anything from
+ # plain HTTP to HTTPS with password authentication.
+ ## url => 'https://user:password@host/write?db=travelynx',
+ },
+
+ mail => {
+ # To disable outgoing mail for development purposes, uncomment the
+ # following line. Mails will instead be logged as Mojolicious "info"
+ # messages, causing their content to be printed on stdout.
+ ## disabled => 1,
+
+ # Otherwise, specify the sender ("From" field) for mail sent by travelynx
+ # here. E.g. 'Travelynx <mail@example.org>'
+ from => die("Changeme!"),
+ },
+
+ registration => {
+ # To disable registration for your instance, uncomment the following
+ # line.
+ ## disabled => 1,
+
+ # To block registration from certain IPs, uncomment the following line
+ # and point it to a file containing one IPv4 or IPv6 address per line.
+ # Blocking IP ranges is not supported.
+ ## denylist => "denylist.txt",
+ },
+
+ # Links to source code and issue tracker shown on the about page.
+ # Please change them if you are using a fork.
+ ref => {
+ # Optional
+ issues => 'https://github.com/derf/travelynx/issues',
+
+ # Mandatory
+ source => 'https://github.com/derf/travelynx',
+ },
+
# Secrets used for cookie signing and verification. Must contain at least
# one random string. If you specify several strings, the first one will
# be used for signing new cookies, and the remaining ones will still be
@@ -49,5 +97,46 @@
die("Changeme!"),
],
+ # optionally, users can link travelynx and traewelling accounts, and
+ # automatically synchronize check-ins.
+ # To do so, you need to create a travelynx application on
+ # <https://traewelling.de/settings/applications>. The application
+ # must be marked as "Confidential" and have a redirect URL that matches
+ # $base_url/oauth/traewelling, where $base_url refers to the URL configured
+ # above. For instance, travelynx.de uses
+ # 'https://travelynx.de/oauth/traewelling'. An incorrect redirect URL will
+ # cause OAuth2 to fail with unsupported_grant_type.
+ #
+ # Note that the travelynx/traewelling OAuth2 integration does not support
+ # travelynx installations that are reachable on multiple URLs at the
+ # moment -- linking a traewelling account is only possible when accessing
+ # travelynx via the base URL.
+ traewelling => {
+
+ # Uncomment the following block and insert the application ID and
+ # secret obtained from https://traewelling.de/settings/applications
+ # -> your application -> Edit.
+
+ #oauth => {
+ # id => 1234,
+ # secret => 'mysecret',
+ #}
+
+ # By default, the "work" or "worker" command does not just update
+ # real-time data of active journeys, but also performs push and pull
+ # synchronization with traewelling for accounts that have configured it.
+ # Traewelling pull synchronization currently relies on polling the user
+ # status on traewelling.de, so large travelynx instances may want to
+ # run pull synchronization less frequently than regular "work" commands
+ # and traewelling push synchronization.
+ #
+ # To do so, uncomment "separate_worker" below and create a cronjob that
+ # periodically runs "perl index.pl traewelling" (push and pull) or
+ # two separate cronjobs that run "perl index.pl traewelling push" and
+ # "perl index.pl traewelling pull", respectively.
+
+ # separate_worker => 1,
+ },
+
version => qx{git describe --dirty} // 'experimental',
};
diff --git a/index.pl b/index.pl
index 730b640..4067b24 100644
--- a/index.pl
+++ b/index.pl
@@ -1,4 +1,7 @@
#!/usr/bin/env perl
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use strict;
use warnings;
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 3cfc675..4749d65 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -1,4 +1,9 @@
package Travelynx;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
use Mojo::Base 'Mojolicious';
use Mojo::Pg;
@@ -8,21 +13,29 @@ use Cache::File;
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
use DateTime;
use DateTime::Format::Strptime;
-use Encode qw(decode encode);
-use Geo::Distance;
+use Encode qw(decode encode);
+use File::Slurp qw(read_file);
use JSON;
use List::Util;
-use List::MoreUtils qw(after_incl before_incl);
+use List::UtilsBy qw(uniq_by);
+use List::MoreUtils qw(first_index);
use Travel::Status::DE::DBWagenreihung;
-use Travel::Status::DE::IRIS;
-use Travel::Status::DE::IRIS::Stations;
+use Travelynx::Helper::DBDB;
+use Travelynx::Helper::HAFAS;
+use Travelynx::Helper::IRIS;
use Travelynx::Helper::Sendmail;
-use XML::LibXML;
+use Travelynx::Helper::Traewelling;
+use Travelynx::Model::InTransit;
+use Travelynx::Model::Journeys;
+use Travelynx::Model::JourneyStatsCache;
+use Travelynx::Model::Stations;
+use Travelynx::Model::Traewelling;
+use Travelynx::Model::Users;
sub check_password {
my ( $password, $hash ) = @_;
- if ( bcrypt( $password, $hash ) eq $hash ) {
+ if ( bcrypt( substr( $password, 0, 10000 ), $hash ) eq $hash ) {
return 1;
}
return 0;
@@ -37,22 +50,11 @@ sub epoch_to_dt {
return DateTime->from_epoch(
epoch => $epoch,
- time_zone => 'Europe/Berlin'
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
);
}
-sub get_station {
- my ($station_name) = @_;
-
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
-
- if ( @candidates == 1 ) {
- return $candidates[0];
- }
- return undef;
-}
-
sub startup {
my ($self) = @_;
@@ -60,6 +62,7 @@ sub startup {
$self->defaults( layout => 'default' );
+ $self->types->type( csv => 'text/csv; charset=utf-8' );
$self->types->type( json => 'application/json; charset=utf-8' );
$self->plugin('Config');
@@ -68,6 +71,9 @@ sub startup {
$self->secrets( $self->config->{secrets} );
}
+ chomp $self->config->{version};
+ $self->defaults( version => $self->config->{version} // 'UNKNOWN' );
+
$self->plugin(
authentication => {
autoload_user => 1,
@@ -78,7 +84,8 @@ sub startup {
},
validate_user => sub {
my ( $self, $username, $password, $extradata ) = @_;
- my $user_info = $self->get_user_password($username);
+ my $user_info
+ = $self->users->get_login_data( name => $username );
if ( not $user_info ) {
return undef;
}
@@ -93,6 +100,23 @@ sub startup {
},
}
);
+
+ if ( my $oa = $self->config->{traewelling}{oauth} ) {
+ $self->plugin(
+ OAuth2 => {
+ providers => {
+ traewelling => {
+ key => $oa->{id},
+ secret => $oa->{secret},
+ authorize_url =>
+'https://traewelling.de/oauth/authorize?response_type=code',
+ token_url => 'https://traewelling.de/oauth/token',
+ }
+ }
+ }
+ );
+ }
+
$self->sessions->default_expiration( 60 * 60 * 24 * 180 );
# Starting with v8.11, Mojolicious sends SameSite=Lax Cookies by default.
@@ -100,7 +124,7 @@ sub startup {
# security and usability for websites that want to maintain user's logged-in
# session after the user arrives from an external link". In practice,
# Safari (both iOS and macOS) does not send a SameSite=lax cookie when
- # following a link from an external site. So, marudor.de providing a
+ # following a link from an external site. So, bahn.expert providing a
# checkin link to travelynx.de/s/whatever does not work because the user
# is not logged in due to Safari not sending the cookie.
#
@@ -116,10 +140,10 @@ sub startup {
before_dispatch => sub {
my ($self) = @_;
- # The "theme" cookie is set client-side if the theme we delivered was
- # changed by dark mode detection or by using the theme switcher. It's
- # not part of Mojolicious' session data (and can't be, due to
- # signing and HTTPOnly), so we need to add it here.
+ # The "theme" cookie is set client-side if the theme we delivered was
+ # changed by dark mode detection or by using the theme switcher. It's
+ # not part of Mojolicious' session data (and can't be, due to
+ # signing and HTTPOnly), so we need to add it here.
for my $cookie ( @{ $self->req->cookies } ) {
if ( $cookie->name eq 'theme' ) {
$self->session( theme => $cookie->value );
@@ -154,76 +178,132 @@ sub startup {
);
$self->attr(
- token_type => sub {
- return {
- status => 1,
- history => 2,
- travel => 3,
- import => 4,
- };
- }
- );
- $self->attr(
- token_types => sub {
- return [qw(status history travel import)];
+ coordinates_by_station => sub {
+ my $legacy_names = $self->app->renamed_station;
+ my $location = $self->stations->get_latlon_by_name;
+ while ( my ( $old_name, $new_name ) = each %{$legacy_names} ) {
+ $location->{$old_name} = $location->{$new_name};
+ }
+ return $location;
}
);
+ # https://de.wikipedia.org/wiki/Liste_nach_Gemeinden_und_Regionen_benannter_IC/ICE-Fahrzeuge#Namensgebung_ICE-Triebz%C3%BCge_nach_Gemeinden
+ # via https://github.com/marudor/bahn.expert/blob/main/src/server/coachSequence/TrainNames.ts
$self->attr(
- account_public_mask => sub {
- return {
- status_intern => 0x01,
- status_extern => 0x02,
- status_comment => 0x04,
- };
+ ice_name => sub {
+ my $id_to_name = JSON->new->utf8->decode(
+ scalar read_file('share/ice_names.json') );
+ return $id_to_name;
}
);
$self->attr(
- journey_edit_mask => sub {
- return {
- sched_departure => 0x0001,
- real_departure => 0x0002,
- route => 0x0010,
- is_cancelled => 0x0020,
- sched_arrival => 0x0100,
- real_arrival => 0x0200,
- };
+ renamed_station => sub {
+ my $legacy_to_new = JSON->new->utf8->decode(
+ scalar read_file('share/old_station_names.json') );
+ return $legacy_to_new;
}
);
- $self->attr(
- coordinates_by_station => sub {
- my %location;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
+ if ( not $self->app->config->{base_url} ) {
+ $self->app->log->error(
+"travelynx.conf: 'base_url' is missing. Links in maintenance/work/worker-generated E-Mails will be incorrect. This variable was introduced in travelynx 1.22; see examples/travelynx.conf for documentation."
+ );
+ }
+
+ $self->helper(
+ base_url_for => sub {
+ my ( $self, $path ) = @_;
+ if ( ( my $url = $self->url_for($path) )->base ne q{}
+ or not $self->app->config->{base_url} )
{
- if ( $station->[3] ) {
- $location{ $station->[1] }
- = [ $station->[4], $station->[3] ];
- }
+ return $url;
}
- return \%location;
+ return $self->url_for($path)
+ ->base( $self->app->config->{base_url} );
}
);
- $self->attr(
- station_by_eva => sub {
- my %map;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
- {
- $map{ $station->[2] } = $station;
- }
- return \%map;
+ $self->helper(
+ hafas => sub {
+ my ($self) = @_;
+ state $hafas = Travelynx::Helper::HAFAS->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
}
);
$self->helper(
- sendmail => sub {
- state $sendmail = Travelynx::Helper::Sendmail->new(
- config => ( $self->config->{mail} // {} ),
- log => $self->log
+ iris => sub {
+ my ($self) = @_;
+ state $iris = Travelynx::Helper::IRIS->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,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
+ traewelling => sub {
+ my ($self) = @_;
+ state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ traewelling_api => sub {
+ my ($self) = @_;
+ state $trwl_api = Travelynx::Helper::Traewelling->new(
+ log => $self->app->log,
+ model => $self->traewelling,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
+ in_transit => sub {
+ my ($self) = @_;
+ state $in_transit = Travelynx::Model::InTransit->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ );
+ }
+ );
+
+ $self->helper(
+ journey_stats_cache => sub {
+ my ($self) = @_;
+ state $journey_stats_cache
+ = Travelynx::Model::JourneyStatsCache->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ );
+ }
+ );
+
+ $self->helper(
+ journeys => sub {
+ my ($self) = @_;
+ state $journeys = Travelynx::Model::Journeys->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ in_transit => $self->in_transit,
+ stats_cache => $self->journey_stats_cache,
+ renamed_station => $self->app->renamed_station,
+ latlon_by_station => $self->app->coordinates_by_station,
+ stations => $self->stations,
);
}
);
@@ -242,6 +322,52 @@ sub startup {
state $pg
= Mojo::Pg->new("postgresql://${user}\@${host}:${port}/${dbname}")
->password($pw);
+
+ $pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do("set time zone 'Europe/Berlin'");
+ }
+ );
+
+ return $pg;
+ }
+ );
+
+ $self->helper(
+ sendmail => sub {
+ state $sendmail = Travelynx::Helper::Sendmail->new(
+ config => ( $self->config->{mail} // {} ),
+ log => $self->log
+ );
+ }
+ );
+
+ $self->helper(
+ stations => sub {
+ my ($self) = @_;
+ state $stations
+ = Travelynx::Model::Stations->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ users => sub {
+ my ($self) = @_;
+ state $users = Travelynx::Model::Users->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ dbdb => sub {
+ my ($self) = @_;
+ state $dbdb = Travelynx::Helper::DBDB->new(
+ log => $self->app->log,
+ cache => $self->app->cache_iris_main,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
}
);
@@ -268,248 +394,256 @@ sub startup {
);
$self->helper(
- 'get_departures' => sub {
- my ( $self, $station, $lookbehind, $lookahead, $with_related ) = @_;
-
- $lookbehind //= 180;
- $lookahead //= 30;
- $with_related //= 0;
-
- my @station_matches
- = Travel::Status::DE::IRIS::Stations::get_station($station);
-
- if ( @station_matches == 1 ) {
- $station = $station_matches[0][0];
- my $status = Travel::Status::DE::IRIS->new(
- station => $station,
- main_cache => $self->app->cache_iris_main,
- realtime_cache => $self->app->cache_iris_rt,
- keep_transfers => 1,
- lookbehind => 20,
- datetime => DateTime->now( time_zone => 'Europe/Berlin' )
- ->subtract( minutes => $lookbehind ),
- lookahead => $lookbehind + $lookahead,
- lwp_options => {
- timeout => 10,
- agent => 'travelynx/' . $self->app->config->{version},
- },
- with_related => $with_related,
- );
- return {
- results => [ $status->results ],
- errstr => $status->errstr,
- station_ds100 =>
- ( $status->station ? $status->station->{ds100} : undef ),
- station_eva =>
- ( $status->station ? $status->station->{uic} : undef ),
- station_name =>
- ( $status->station ? $status->station->{name} : undef ),
- related_stations => [ $status->related_stations ],
- };
- }
- elsif ( @station_matches > 1 ) {
- return {
- results => [],
- errstr => 'Mehrdeutiger Stationsname. Mögliche Eingaben: '
- . join( q{, }, map { $_->[1] } @station_matches ),
- };
+ 'sprintf_km' => sub {
+ my ( $self, $km ) = @_;
+
+ if ( $km < 1 ) {
+ return sprintf( '%.f m', $km * 1000 );
}
- else {
- return {
- results => [],
- errstr => 'Unbekannte Station',
- };
+ if ( $km < 10 ) {
+ return sprintf( '%.1f km', $km );
}
+ return sprintf( '%.f km', $km );
}
);
$self->helper(
- 'grep_unknown_stations' => sub {
- my ( $self, @stations ) = @_;
-
- my @unknown_stations;
- for my $station (@stations) {
- my $station_info = get_station($station);
- if ( not $station_info ) {
- push( @unknown_stations, $station );
- }
+ 'load_icon' => sub {
+ my ( $self, $load ) = @_;
+ my $first = $load->{FIRST} // 0;
+ my $second = $load->{SECOND} // 0;
+
+ my @symbols
+ = (
+ qw(help_outline person_outline people priority_high not_interested)
+ );
+
+ return ( $symbols[$first], $symbols[$second] );
+ }
+ );
+
+ $self->helper(
+ 'visibility_icon' => sub {
+ my ( $self, $visibility ) = @_;
+ if ( $visibility eq 'public' ) {
+ return 'language';
+ }
+ if ( $visibility eq 'travelynx' ) {
+ return 'lock_open';
+ }
+ if ( $visibility eq 'followers' ) {
+ return 'group';
+ }
+ if ( $visibility eq 'unlisted' ) {
+ return 'lock_outline';
}
- return @unknown_stations;
+ if ( $visibility eq 'private' ) {
+ return 'lock';
+ }
+ return 'help_outline';
}
);
- # Returns (journey id, error)
- # Must be called during a transaction.
- # Must perform a rollback on error.
$self->helper(
- 'add_journey' => sub {
+ 'checkin_p' => sub {
my ( $self, %opt ) = @_;
- my $db = $opt{db};
- my $uid = $opt{uid} // $self->current_user->{id};
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $dep_station = get_station( $opt{dep_station} );
- my $arr_station = get_station( $opt{arr_station} );
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
- if ( not $dep_station ) {
- return ( undef, 'Unbekannter Startbahnhof' );
+ my $user = $self->get_user_status( $uid, $db );
+ if ( $user->{checked_in} or $user->{cancelled} ) {
+ return Mojo::Promise->reject('You are already checked in');
}
- if ( not $arr_station ) {
- return ( undef, 'Unbekannter Zielbahnhof' );
+
+ if ( $train_id =~ m{[|]} ) {
+ return $self->_checkin_hafas_p(%opt);
}
- my @route = ( [ $dep_station->[1], {}, undef ] );
+ my $promise = Mojo::Promise->new;
- if ( $opt{route} ) {
- my @unknown_stations;
- for my $station ( @{ $opt{route} } ) {
- my $station_info = get_station($station);
- if ($station_info) {
- push( @route, [ $station_info->[1], {}, undef ] );
- }
- else {
- push( @route, [ $station, {}, undef ] );
- push( @unknown_stations, $station );
- }
- }
+ $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 140,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($status) = @_;
- if ( not $opt{lax} ) {
- if ( @unknown_stations == 1 ) {
- return ( undef,
- "Unbekannter Unterwegshalt: $unknown_stations[0]" );
- }
- elsif (@unknown_stations) {
- return ( undef,
- 'Unbekannte Unterwegshalte: '
- . join( ', ', @unknown_stations ) );
+ if ( $status->{errstr} ) {
+ $promise->reject( $status->{errstr} );
+ return;
}
- }
- }
-
- push( @route, [ $arr_station->[1], {}, undef ] );
-
- if ( $route[0][0] eq $route[1][0] ) {
- shift(@route);
- }
- if ( $route[-2][0] eq $route[-1][0] ) {
- pop(@route);
- }
+ my $eva = $status->{station_eva};
+ my $train = List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- my $entry = {
- user_id => $uid,
- train_type => $opt{train_type},
- train_line => $opt{train_line},
- train_no => $opt{train_no},
- train_id => 'manual',
- checkin_station_id => $dep_station->[2],
- checkin_time => $now,
- sched_departure => $opt{sched_departure},
- real_departure => $opt{rt_departure},
- checkout_station_id => $arr_station->[2],
- sched_arrival => $opt{sched_arrival},
- real_arrival => $opt{rt_arrival},
- checkout_time => $now,
- edited => 0x3fff,
- cancelled => $opt{cancelled} ? 1 : 0,
- route => JSON->new->encode( \@route ),
- };
+ if ( not defined $train ) {
+ $promise->reject("Train ${train_id} not found");
+ return;
+ }
- if ( $opt{comment} ) {
- $entry->{user_data}
- = JSON->new->encode( { comment => $opt{comment} } );
- }
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ departure_eva => $eva,
+ train => $train,
+ route => [ $self->iris->route_diff($train) ],
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
- my $journey_id = undef;
- eval {
- $journey_id
- = $db->insert( 'journeys', $entry, { returning => 'id' } )
- ->hash->{id};
- $self->invalidate_stats_cache( $opt{rt_departure}, $db, $uid );
- };
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->add_route_timestamps( $uid, $train, 1 );
+ $self->run_hook( $uid, 'checkin' );
+ }
- if ($@) {
- $self->app->log->error("add_journey($uid): $@");
- return ( undef, 'add_journey failed: ' . $@ );
- }
+ $promise->resolve($train);
+ return;
+ }
+ )->catch(
+ sub {
+ my ( $err, $status ) = @_;
+ $promise->reject( $status->{errstr} );
+ return;
+ }
+ )->wait;
- return ( $journey_id, undef );
+ return $promise;
}
);
$self->helper(
- 'checkin' => sub {
- my ( $self, $station, $train_id, $uid ) = @_;
+ '_checkin_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
- $uid //= $self->current_user->{id};
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
- my $status = $self->get_departures( $station, 140, 40, 0 );
- if ( $status->{errstr} ) {
- return ( undef, $status->{errstr} );
- }
- else {
- my ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
- if ( not defined $train ) {
- return ( undef, "Train ${train_id} not found" );
- }
- else {
-
- my $user = $self->get_user_status($uid);
- if ( $user->{checked_in} or $user->{cancelled} ) {
+ my $promise = Mojo::Promise->new;
- if ( $user->{train_id} eq $train_id
- and $user->{dep_eva} eq $status->{station_eva} )
+ $self->hafas->get_journey_p(
+ trip_id => $train_id,
+ with_polyline => 1
+ )->then(
+ sub {
+ my ($journey) = @_;
+ my $found;
+ for my $stop ( $journey->route ) {
+ if ( $stop->loc->name eq $station
+ or $stop->loc->eva == $station )
{
- # checking in twice is harmless
- return ( $train, undef );
+ $found = $stop;
+ last;
}
-
- # Otherwise, someone forgot to check out first
- $self->checkout( $station, 1, $uid );
}
-
+ if ( not $found ) {
+ $promise->reject(
+ "Did not find journey $train_id at $station");
+ return;
+ }
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ );
+ }
eval {
- my $json = JSON->new;
- $self->pg->db->insert(
- 'in_transit',
- {
- user_id => $uid,
- cancelled => $train->departure_is_cancelled
- ? 1
- : 0,
- checkin_station_id => $status->{station_eva},
- checkin_time =>
- DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $train->platform,
- train_type => $train->type,
- train_line => $train->line_no,
- train_no => $train->train_no,
- train_id => $train->train_id,
- sched_departure => $train->sched_departure,
- real_departure => $train->departure,
- route => $json->encode(
- [ $self->route_diff($train) ]
- ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- )
- }
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
);
};
if ($@) {
$self->app->log->error(
"Checkin($uid): INSERT failed: $@");
- return ( undef, 'INSERT failed: ' . $@ );
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => { trip_id => $journey->id }
+ );
+
+ my $polyline;
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{name} ) {
+ push(
+ @coordinate_list,
+ [
+ $coord->{lon}, $coord->{lat},
+ $coord->{eva}
+ ]
+ );
+ push( @station_list, $coord->{name} );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $journey->line
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->loc->eva,
+ to_eva => ( $journey->route )[-1]->loc->eva,
+ coords => \@coordinate_list,
+ };
+ }
}
- $self->add_route_timestamps( $uid, $train, 1 );
- $self->run_hook( $uid, 'checkin' );
- return ( $train, undef );
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ }
+
+ $promise->resolve($journey);
}
- }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
}
);
@@ -519,9 +653,7 @@ sub startup {
$uid //= $self->current_user->{id};
if ( $journey_id eq 'in_transit' ) {
- eval {
- $self->pg->db->delete( 'in_transit', { user_id => $uid } );
- };
+ eval { $self->in_transit->delete( uid => $uid ); };
if ($@) {
$self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@";
@@ -537,20 +669,10 @@ sub startup {
my $db = $self->pg->db;
my $tx = $db->begin;
- my $journey = $db->select(
- 'journeys',
- '*',
- {
- user_id => $uid,
- id => $journey_id
- }
- )->hash;
- $db->delete(
- 'journeys',
- {
- user_id => $uid,
- id => $journey_id
- }
+ my $journey = $self->journeys->pop(
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id
);
if ( $journey->{edited} ) {
@@ -562,7 +684,30 @@ sub startup {
delete $journey->{edited};
delete $journey->{id};
- $db->insert( 'in_transit', $journey );
+ # users may force checkouts at stations that are not part of
+ # the train's scheduled (or real-time) route. re-adding those
+ # to in-transit violates the assumption that each train has
+ # a valid destination. Remove the target in this case.
+ my $route = JSON->new->decode( $journey->{route} );
+ my $found_checkout_id;
+ for my $stop ( @{$route} ) {
+ if ( $stop->[1] == $journey->{checkout_station_id} ) {
+ $found_checkout_id = 1;
+ last;
+ }
+ }
+ if ( not $found_checkout_id ) {
+ $journey->{checkout_station_id} = undef;
+ $journey->{checkout_time} = undef;
+ $journey->{arr_platform} = undef;
+ $journey->{sched_arrival} = undef;
+ $journey->{real_arrival} = undef;
+ }
+
+ $self->in_transit->add_from_journey(
+ db => $db,
+ journey => $journey
+ );
my $cache_ts = DateTime->now( time_zone => 'Europe/Berlin' );
if ( $journey->{real_departure}
@@ -574,7 +719,11 @@ sub startup {
);
}
- $self->invalidate_stats_cache( $cache_ts, $db, $uid );
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
$tx->commit;
};
@@ -587,628 +736,395 @@ sub startup {
}
);
- # Statistics are partitioned by real_departure, which must be provided
- # when calling this function e.g. after journey deletion or editing.
- # If a joureny's real_departure has been edited, this function must be
- # called twice: once with the old and once with the new value.
$self->helper(
- 'invalidate_stats_cache' => sub {
- my ( $self, $ts, $db, $uid ) = @_;
-
- $uid //= $self->current_user->{id};
- $db //= $self->pg->db;
+ 'checkout_p' => sub {
+ my ( $self, %opt ) = @_;
- $self->pg->db->delete(
- 'journey_stats',
- {
- user_id => $uid,
- year => $ts->year,
- month => $ts->month,
- }
- );
- $self->pg->db->delete(
- 'journey_stats',
- {
- user_id => $uid,
- year => $ts->year,
- month => 0,
- }
- );
- }
- );
+ my $station = $opt{station};
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $with_related = $opt{with_related} // 0;
+ my $force = $opt{force};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $user = $self->get_user_status( $uid, $db );
+ my $train_id = $user->{train_id};
- $self->helper(
- 'checkout' => sub {
- my ( $self, $station, $force, $uid ) = @_;
+ my $promise = Mojo::Promise->new;
- my $db = $self->pg->db;
- my $status = $self->get_departures( $station, 120, 120, 0 );
- $uid //= $self->current_user->{id};
- my $user = $self->get_user_status($uid);
- my $train_id = $user->{train_id};
+ if ( not $station ) {
+ $self->app->log->error("Checkout($uid): station is empty");
+ return $promise->resolve( 1,
+ 'BUG: Checkout station is empty.' );
+ }
if ( not $user->{checked_in} and not $user->{cancelled} ) {
- return ( 0, 'You are not checked into any train' );
- }
- if ( $status->{errstr} and not $force ) {
- return ( 1, $status->{errstr} );
+ return $promise->resolve( 0,
+ 'You are not checked into any train' );
}
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $journey
- = $db->select( 'in_transit', '*', { user_id => $uid } )
- ->expand->hash;
-
- # 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 > $user->{sched_departure}->epoch
+ if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
}
- @{ $status->{results} };
-
- $train //= List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
-
- my $new_checkout_station_id = $status->{station_eva};
-
- # When a checkout is triggered by a checkin, there is an edge case
- # with related stations.
- # Assume a user travels from A to B1, then from B2 to C. B1 and B2 are
- # relatd stations (e.g. "Frankfurt Hbf" and "Frankfurt Hbf(tief)").
- # Now, if they check in for the journey from B2 to C, and have not yet
- # checked out of the previous train, $train is undef as B2 is not B1.
- # Redo the request with with_related => 1 to avoid this case.
- # While at it, we increase the lookahead to handle long journeys as
- # well.
- if ( not $train ) {
- $status = $self->get_departures( $station, 120, 180, 1 );
- ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
- if ( $train
- and $self->app->station_by_eva->{ $train->station_uic } )
- {
- $new_checkout_station_id = $train->station_uic;
- }
+ if ( $arr_eva and $arr_eva != $user->{arr_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
}
- # Store the intended checkout station regardless of this operation's
- # success.
- $db->update(
- 'in_transit',
- {
- checkout_station_id => $new_checkout_station_id,
- },
- { user_id => $uid }
- );
-
- # If in_transit already contains arrival data for another estimated
- # destination, we must invalidate it.
- if ( defined $journey->{checkout_station_id}
- and $journey->{checkout_station_id}
- != $new_checkout_station_id )
- {
- $db->update(
- 'in_transit',
- {
- checkout_time => undef,
- arr_platform => undef,
- sched_arrival => undef,
- real_arrival => undef,
- },
- { user_id => $uid }
- );
+ if ( $train_id =~ m{[|]} ) {
+ return $self->_checkout_hafas_p(%opt);
}
- if ( not defined $train ) {
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $journey = $self->in_transit->get(
+ uid => $uid,
+ with_data => 1
+ );
- # Arrival time via IRIS is unknown, so the train probably has not
- # arrived yet. Fall back to HAFAS.
- # TODO support cases where $station is EVA or DS100 code
- if (
- my $station_data
- = List::Util::first { $_->[0] eq $station }
- @{ $journey->{route} }
- )
- {
- $station_data = $station_data->[1];
- if ( $station_data->{sched_arr} ) {
- my $sched_arr
- = epoch_to_dt( $station_data->{sched_arr} );
- my $rt_arr = $sched_arr->clone;
- if ( $station_data->{adelay}
- and $station_data->{adelay} =~ m{^\d+$} )
- {
- $rt_arr->add( minutes => $station_data->{adelay} );
- }
- $db->update(
- 'in_transit',
- {
- sched_arrival => $sched_arr,
- real_arrival => $rt_arr
- },
- { user_id => $uid }
+ $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 120,
+ lookahead => 180,
+ with_related => $with_related,
+ )->then(
+ sub {
+ my ($status) = @_;
+
+ my $new_checkout_station_id = $status->{station_eva};
+
+ # Store the intended checkout station regardless of this operation's
+ # success.
+ # TODO for with_related == 1, the correct EVA may be different
+ # and should be fetched from $train later on
+ $self->in_transit->set_arrival_eva(
+ uid => $uid,
+ db => $db,
+ arrival_eva => $new_checkout_station_id
+ );
+
+ # If in_transit already contains arrival data for another estimated
+ # destination, we must invalidate it.
+ if ( defined $journey->{checkout_station_id}
+ and $journey->{checkout_station_id}
+ != $new_checkout_station_id )
+ {
+ $self->in_transit->unset_arrival_data(
+ uid => $uid,
+ db => $db
);
}
- }
- if ( not $force ) {
- $self->run_hook( $uid, 'update' );
- return ( 1, undef );
- }
- }
-
- my $has_arrived = 0;
- eval {
-
- my $tx = $db->begin;
+ # 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
+ > $user->{sched_departure}->epoch
+ }
+ @{ $status->{results} };
- if ( defined $train ) {
+ $train //= List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- if ( not $train->arrival ) {
- die("Train has no arrival timestamp\n");
- }
+ if ( not defined $train ) {
- $has_arrived = $train->arrival->epoch < $now->epoch ? 1 : 0;
- my $json = JSON->new;
- $db->update(
- 'in_transit',
+ # Arrival time via IRIS is unknown, so the train probably
+ # has not arrived yet. Fall back to HAFAS.
+ # TODO support cases where $station is EVA or DS100 code
+ if (
+ my $station_data
+ = List::Util::first { $_->[0] eq $station }
+ @{ $journey->{route} }
+ )
{
- checkout_time => $now,
- arr_platform => $train->platform,
- sched_arrival => $train->sched_arrival,
- real_arrival => $train->arrival,
- cancelled => $train->arrival_is_cancelled ? 1 : 0,
- route =>
- $json->encode( [ $self->route_diff($train) ] ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- )
- },
- { user_id => $uid }
- );
- if ($has_arrived) {
- my @unknown_stations
- = $self->grep_unknown_stations( $train->route );
- if (@unknown_stations) {
- $self->app->log->warn(
- 'Encountered unknown stations: '
- . join( ', ', @unknown_stations ) );
+ $station_data = $station_data->[2];
+ if ( $station_data->{sched_arr} ) {
+ my $sched_arr
+ = epoch_to_dt( $station_data->{sched_arr} );
+ my $rt_arr
+ = epoch_to_dt( $station_data->{rt_arr} );
+ if ( $rt_arr->epoch == 0 ) {
+ $rt_arr = $sched_arr->clone;
+ if ( $station_data->{arr_delay}
+ and $station_data->{arr_delay}
+ =~ m{^\d+$} )
+ {
+ $rt_arr->add( minutes =>
+ $station_data->{arr_delay} );
+ }
+ }
+ $self->in_transit->set_arrival_times(
+ uid => $uid,
+ db => $db,
+ sched_arrival => $sched_arr,
+ rt_arrival => $rt_arr
+ );
+ }
}
- }
- }
+ if ( not $force ) {
- $journey
- = $db->select( 'in_transit', '*', { user_id => $uid } )->hash;
-
- if ( $has_arrived or $force ) {
- delete $journey->{data};
- $journey->{edited} = 0;
- $journey->{checkout_time} = $now;
- $db->insert( 'journeys', $journey );
- $db->delete( 'in_transit', { user_id => $uid } );
-
- my $cache_ts = $now->clone;
- if ( $journey->{real_departure}
- =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x )
- {
- $cache_ts->set(
- year => $+{year},
- month => $+{month}
- );
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ }
+ $promise->resolve( 1, undef );
+ return;
+ }
}
- $self->invalidate_stats_cache( $cache_ts, $db, $uid );
- }
-
- $tx->commit;
- };
-
- if ($@) {
- $self->app->log->error("Checkout($uid): $@");
- return ( 1, 'Checkout error: ' . $@ );
- }
+ my $has_arrived = 0;
- if ( $has_arrived or $force ) {
- $self->run_hook( $uid, 'checkout' );
- return ( 0, undef );
- }
- $self->run_hook( $uid, 'update' );
- $self->add_route_timestamps( $uid, $train, 0 );
- return ( 1, undef );
- }
- );
+ eval {
- $self->helper(
- 'mark_seen' => sub {
- my ( $self, $uid ) = @_;
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
- $self->pg->db->update(
- 'users',
- { last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) },
- { id => $uid }
- );
- }
- );
+ if ( defined $train
+ and not $train->arrival
+ and not $force )
+ {
+ my $train_no = $train->train_no;
+ die("Train ${train_no} has no arrival timestamp\n");
+ }
+ elsif ( defined $train and $train->arrival ) {
+ $self->in_transit->set_arrival(
+ uid => $uid,
+ db => $db,
+ train => $train,
+ route => [ $self->iris->route_diff($train) ]
+ );
- $self->helper(
- 'update_in_transit_comment' => sub {
- my ( $self, $comment, $uid ) = @_;
- $uid //= $self->current_user->{id};
+ $has_arrived
+ = $train->arrival->epoch < $now->epoch ? 1 : 0;
+ if ($has_arrived) {
+ my @unknown_stations
+ = $self->stations->grep_unknown(
+ $train->route );
+ if (@unknown_stations) {
+ $self->app->log->warn(
+ sprintf(
+'Route of %s %s (%s -> %s) contains unknown stations: %s',
+ $train->type,
+ $train->train_no,
+ $train->origin,
+ $train->destination,
+ join( ', ', @unknown_stations )
+ )
+ );
+ }
+ }
+ }
- my $status = $self->pg->db->select( 'in_transit', ['user_data'],
- { user_id => $uid } )->expand->hash;
- if ( not $status ) {
- return;
- }
- $status->{user_data}{comment} = $comment;
- $self->pg->db->update(
- 'in_transit',
- { user_data => JSON->new->encode( $status->{user_data} ) },
- { user_id => $uid }
- );
- }
- );
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
- $self->helper(
- 'update_journey_part' => sub {
- my ( $self, $db, $journey_id, $key, $value ) = @_;
- my $rows;
-
- my $journey = $self->get_journey(
- db => $db,
- journey_id => $journey_id,
- with_datetime => 1,
- );
+ if ( $has_arrived or $force ) {
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->delete(
+ uid => $uid,
+ db => $db
+ );
- eval {
- if ( $key eq 'sched_departure' ) {
- $rows = $db->update(
- 'journeys',
- {
- sched_departure => $value,
- edited => $journey->{edited} | 0x0001,
- },
- {
- id => $journey_id,
+ my $cache_ts = $now->clone;
+ if ( $journey->{real_departure}
+ =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x
+ )
+ {
+ $cache_ts->set(
+ year => $+{year},
+ month => $+{month}
+ );
+ }
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
}
- )->rows;
- }
- elsif ( $key eq 'rt_departure' ) {
- $rows = $db->update(
- 'journeys',
- {
- real_departure => $value,
- edited => $journey->{edited} | 0x0002,
- },
+ elsif ( defined $train
+ and $train->arrival_is_cancelled )
{
- id => $journey_id,
- }
- )->rows;
- # stats are partitioned by rt_departure -> both the cache for
- # the old value (see bottom of this function) and the new value
- # (here) must be invalidated.
- $self->invalidate_stats_cache( $value, $db );
- }
- elsif ( $key eq 'sched_arrival' ) {
- $rows = $db->update(
- 'journeys',
- {
- sched_arrival => $value,
- edited => $journey->{edited} | 0x0100,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'rt_arrival' ) {
- $rows = $db->update(
- 'journeys',
- {
- real_arrival => $value,
- edited => $journey->{edited} | 0x0200,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'route' ) {
- my @new_route = map { [ $_, {}, undef ] } @{$value};
- $rows = $db->update(
- 'journeys',
- {
- route => JSON->new->encode( \@new_route ),
- edited => $journey->{edited} | 0x0010,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'cancelled' ) {
- $rows = $db->update(
- 'journeys',
- {
- cancelled => $value,
- edited => $journey->{edited} | 0x0020,
- },
- {
- id => $journey_id,
+ # This branch is only taken if the deparure was not cancelled,
+ # i.e., if the train was supposed to go here but got
+ # redirected or cancelled on the way and not from the start on.
+ # If the departure itself was cancelled, the user route is
+ # cancelled_from action -> 'cancelled journey' panel on main page
+ # -> cancelled_to action -> force checkout (causing the
+ # previous branch to be taken due to $force)
+ $journey->{cancelled} = 1;
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->set_cancelled_destination(
+ uid => $uid,
+ db => $db,
+ cancelled_destination => $train->station,
+ );
}
- )->rows;
- }
- elsif ( $key eq 'comment' ) {
- $journey->{user_data}{comment} = $value;
- $rows = $db->update(
- 'journeys',
- {
- user_data =>
- JSON->new->encode( $journey->{user_data} ),
- },
- {
- id => $journey_id,
+
+ if ( not $opt{in_transaction} ) {
+ $tx->commit;
}
- )->rows;
- }
- else {
- die("Invalid key $key\n");
- }
- };
+ };
- if ($@) {
- $self->app->log->error(
- "update_journey_part($journey_id, $key): $@");
- return "update_journey_part($key): $@";
- }
- if ( $rows == 1 ) {
- $self->invalidate_stats_cache( $journey->{rt_departure}, $db );
- return undef;
- }
- return 'UPDATE failed: did not match any journey part';
- }
- );
+ if ($@) {
+ $self->app->log->error("Checkout($uid): $@");
+ $promise->resolve( 1, 'Checkout error: ' . $@ );
+ return;
+ }
- $self->helper(
- 'journey_sanity_check' => sub {
- my ( $self, $journey, $lax ) = @_;
+ if ( $has_arrived or $force ) {
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkout' );
+ }
+ $promise->resolve( 0, undef );
+ return;
+ }
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ $self->add_route_timestamps( $uid, $train, 0, 1 );
+ }
+ $promise->resolve( 1, undef );
+ return;
- if ( $journey->{sched_duration} and $journey->{sched_duration} < 0 )
- {
- return
-'Die geplante Dauer dieser Zugfahrt ist negativ. Zeitreisen werden aktuell nicht unterstützt.';
- }
- if ( $journey->{rt_duration} and $journey->{rt_duration} < 0 ) {
- return
-'Die Dauer dieser Zugfahrt ist negativ. Zeitreisen werden aktuell nicht unterstützt.';
- }
- if ( $journey->{sched_duration}
- and $journey->{sched_duration} > 60 * 60 * 24 )
- {
- return 'Die Zugfahrt ist länger als 24 Stunden.';
- }
- if ( $journey->{rt_duration}
- and $journey->{rt_duration} > 60 * 60 * 24 )
- {
- return 'Die Zugfahrt ist länger als 24 Stunden.';
- }
- if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 )
- {
- return 'Zugfahrten mit über 500 km/h? Schön wär\'s.';
- }
- if ( $journey->{route} and @{ $journey->{route} } > 99 ) {
- my $stop_count = @{ $journey->{route} };
- return
-"Die Zugfahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
- }
- if ( $journey->{edited} & 0x0010 and not $lax ) {
- my @unknown_stations
- = $self->grep_unknown_stations( map { $_->[0] }
- @{ $journey->{route} } );
- if (@unknown_stations) {
- return 'Unbekannte Station(en): '
- . join( ', ', @unknown_stations );
}
- }
-
- return undef;
- }
- );
-
- $self->helper(
- 'verify_registration_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- my $res = $db->select(
- 'pending_registrations',
- 'count(*) as count',
- {
- user_id => $uid,
- token => $token
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->resolve( 1, $err );
+ return;
}
- );
+ )->wait;
- if ( $res->hash->{count} ) {
- $db->update( 'users', { status => 1 }, { id => $uid } );
- $db->delete( 'pending_registrations', { user_id => $uid } );
- $tx->commit;
- return 1;
- }
- return;
+ return $promise;
}
);
$self->helper(
- 'get_uid_by_name_and_mail' => sub {
- my ( $self, $name, $email ) = @_;
+ '_checkout_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
- my $res = $self->pg->db->select(
- 'users',
- ['id'],
- {
- name => $name,
- email => $email,
- status => 1
- }
- );
+ my $station = $opt{station};
+ my $force = $opt{force};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
- if ( my $user = $res->hash ) {
- return $user->{id};
- }
- return;
- }
- );
-
- $self->helper(
- 'get_privacy_by_name' => sub {
- my ( $self, $name ) = @_;
+ my $promise = Mojo::Promise->new;
- my $res = $self->pg->db->select(
- 'users',
- [ 'id', 'public_level' ],
- {
- name => $name,
- status => 1
- }
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db,
+ with_data => 1,
+ with_timestamps => 1,
+ with_visibility => 1,
+ postprocess => 1,
);
- if ( my $user = $res->hash ) {
- return $user;
+ # with_visibility needed due to postprocess
+
+ my $found;
+ my $has_arrived;
+ for my $stop ( @{ $journey->{route_after} } ) {
+ if ( $station eq $stop->[0] or $station eq $stop->[1] ) {
+ $found = 1;
+ $self->in_transit->set_arrival_eva(
+ uid => $uid,
+ db => $db,
+ arrival_eva => $stop->[1],
+ );
+ if ( defined $journey->{checkout_station_id}
+ and $journey->{checkout_station_id} != $stop->{eva} )
+ {
+ $self->in_transit->unset_arrival_data(
+ uid => $uid,
+ db => $db
+ );
+ }
+ $self->in_transit->set_arrival_times(
+ uid => $uid,
+ db => $db,
+ sched_arrival => $stop->[2]{sched_arr},
+ rt_arrival =>
+ ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} )
+ );
+ if (
+ $now > ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} ) )
+ {
+ $has_arrived = 1;
+ }
+ last;
+ }
}
- return;
- }
- );
-
- $self->helper(
- 'set_privacy' => sub {
- my ( $self, $uid, $public_level ) = @_;
-
- $self->pg->db->update(
- 'users',
- { public_level => $public_level },
- { id => $uid }
- );
- }
- );
-
- $self->helper(
- 'mark_for_password_reset' => sub {
- my ( $self, $db, $uid, $token ) = @_;
-
- my $res = $db->select(
- 'pending_passwords',
- 'count(*) as count',
- { user_id => $uid }
- );
- if ( $res->hash->{count} ) {
- return 'in progress';
+ if ( not $found ) {
+ return $promise->resolve( 1, 'station not found in route' );
}
- $db->insert(
- 'pending_passwords',
- {
- user_id => $uid,
- token => $token,
- requested_at =>
- DateTime->now( time_zone => 'Europe/Berlin' )
+ eval {
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
}
- );
-
- return undef;
- }
- );
- $self->helper(
- 'verify_password_token' => sub {
- my ( $self, $uid, $token ) = @_;
+ if ( $has_arrived or $force ) {
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->delete(
+ uid => $uid,
+ db => $db
+ );
- my $res = $self->pg->db->select(
- 'pending_passwords',
- 'count(*) as count',
- {
- user_id => $uid,
- token => $token
+ my $cache_ts = $now->clone;
+ if ( $journey->{real_departure}
+ =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x )
+ {
+ $cache_ts->set(
+ year => $+{year},
+ month => $+{month}
+ );
+ }
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
}
- );
- if ( $res->hash->{count} ) {
- return 1;
- }
- return;
- }
- );
-
- $self->helper(
- 'mark_for_mail_change' => sub {
- my ( $self, $db, $uid, $email, $token ) = @_;
-
- $db->insert(
- 'pending_mails',
- {
- user_id => $uid,
- email => $email,
- token => $token,
- requested_at =>
- DateTime->now( time_zone => 'Europe/Berlin' )
- },
- {
- on_conflict => \
-'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, requested_at = EXCLUDED.requested_at'
- },
- );
- }
- );
-
- $self->helper(
- 'change_mail_with_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- my $res_h = $db->select(
- 'pending_mails',
- ['email'],
- {
- user_id => $uid,
- token => $token
+ if ($tx) {
+ $tx->commit;
}
- )->hash;
+ };
- if ($res_h) {
- $db->update(
- 'users',
- { email => $res_h->{email} },
- { id => $uid }
- );
- $db->delete( 'pending_mails', { user_id => $uid } );
- $tx->commit;
- return 1;
+ if ($@) {
+ $self->app->log->error("Checkout($uid): $@");
+ return $promise->resolve( 1, 'Checkout error: ' . $@ );
}
- return;
- }
- );
- $self->helper(
- 'remove_password_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- $self->pg->db->delete(
- 'pending_passwords',
- {
- user_id => $uid,
- token => $token
+ if ( $has_arrived or $force ) {
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkout' );
}
- );
+ return $promise->resolve( 0, undef );
+ }
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ }
+ return $promise->resolve( 1, undef );
}
);
@@ -1221,124 +1137,7 @@ sub startup {
$uid //= $self->current_user->{id};
- my $user_data = $self->pg->db->select(
- 'users',
- 'id, name, status, public_level, email, '
- . 'extract(epoch from registered_at) as registered_at_ts, '
- . 'extract(epoch from last_seen) as last_seen_ts, '
- . 'extract(epoch from deletion_requested) as deletion_requested_ts',
- { id => $uid }
- )->hash;
-
- if ($user_data) {
- return {
- id => $user_data->{id},
- name => $user_data->{name},
- status => $user_data->{status},
- is_public => $user_data->{public_level},
- email => $user_data->{email},
- registered_at => DateTime->from_epoch(
- epoch => $user_data->{registered_at_ts},
- time_zone => 'Europe/Berlin'
- ),
- last_seen => DateTime->from_epoch(
- epoch => $user_data->{last_seen_ts},
- time_zone => 'Europe/Berlin'
- ),
- deletion_requested => $user_data->{deletion_requested_ts}
- ? DateTime->from_epoch(
- epoch => $user_data->{deletion_requested_ts},
- time_zone => 'Europe/Berlin'
- )
- : undef,
- };
- }
- return undef;
- }
- );
-
- $self->helper(
- 'get_api_token' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id};
-
- my $token = {};
- my $res = $self->pg->db->select(
- 'tokens',
- [ 'type', 'token' ],
- { user_id => $uid }
- );
-
- for my $entry ( $res->hashes->each ) {
- $token->{ $self->app->token_types->[ $entry->{type} - 1 ] }
- = $entry->{token};
- }
-
- return $token;
- }
- );
-
- $self->helper(
- 'get_webhook' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id};
-
- my $res_h
- = $self->pg->db->select( 'webhooks_str', '*',
- { user_id => $uid } )->hash;
-
- $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} );
-
- return $res_h;
- }
- );
-
- $self->helper(
- 'set_webhook' => sub {
- my ( $self, %opt ) = @_;
-
- $opt{uid} //= $self->current_user->{id};
-
- if ( $opt{token} ) {
- $opt{token} =~ tr{\r\n}{}d;
- }
-
- my $res = $self->pg->db->insert(
- 'webhooks',
- {
- user_id => $opt{uid},
- enabled => $opt{enabled},
- url => $opt{url},
- token => $opt{token}
- },
- {
- on_conflict => \
-'(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null'
- }
- );
- }
- );
-
- $self->helper(
- 'mark_hook_status' => sub {
- my ( $self, $uid, $url, $success, $text ) = @_;
-
- if ( length($text) > 1000 ) {
- $text = substr( $text, 0, 1000 ) . '…';
- }
-
- $self->pg->db->update(
- 'webhooks',
- {
- errored => $success ? 0 : 1,
- latest_run => DateTime->now( time_zone => 'Europe/Berlin' ),
- output => $text,
- },
- {
- user_id => $uid,
- url => $url
- }
- );
+ return $self->users->get( uid => $uid );
}
);
@@ -1346,7 +1145,7 @@ sub startup {
'run_hook' => sub {
my ( $self, $uid, $reason, $callback ) = @_;
- my $hook = $self->get_webhook($uid);
+ my $hook = $self->users->get_webhook( uid => $uid );
if ( not $hook->{enabled} or not $hook->{url} =~ m{^ https?:// }x )
{
@@ -1356,7 +1155,7 @@ sub startup {
return;
}
- my $status = $self->get_user_status_json_v1($uid);
+ my $status = $self->get_user_status_json_v1( uid => $uid );
my $header = {};
my $hook_body = {
reason => $reason,
@@ -1365,6 +1164,8 @@ sub startup {
if ( $hook->{token} ) {
$header->{Authorization} = "Bearer $hook->{token}";
+ $header->{'User-Agent'}
+ = 'travelynx/' . $self->app->config->{version};
}
my $ua = $self->ua;
@@ -1379,804 +1180,194 @@ sub startup {
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- $self->mark_hook_status( $uid, $hook->{url}, 0,
- "HTTP $err->{code} $err->{message}" );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 0,
+ text => "HTTP $err->{code} $err->{message}"
+ );
}
else {
- $self->mark_hook_status( $uid, $hook->{url}, 1,
- $tx->result->body );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 1,
+ text => $tx->result->body
+ );
}
if ($callback) {
&$callback();
}
+ return;
}
)->catch(
sub {
my ($err) = @_;
- $self->mark_hook_status( $uid, $hook->{url}, 0, $err );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 0,
+ text => $err
+ );
if ($callback) {
&$callback();
}
+ return;
}
)->wait;
}
);
- $self->helper(
- 'get_user_password' => sub {
- my ( $self, $name ) = @_;
-
- my $res_h = $self->pg->db->select(
- 'users',
- 'id, name, status, password as password_hash',
- { name => $name }
- )->hash;
-
- return $res_h;
- }
- );
-
- $self->helper(
- 'add_user' => sub {
- my ( $self, $db, $user_name, $email, $token, $password ) = @_;
-
- # This helper must be called during a transaction, as user creation
- # may fail even after the database entry has been generated, e.g. if
- # the registration mail cannot be sent. We therefore use $db (the
- # database handle performing the transaction) instead of $self->pg->db
- # (which may be a new handle not belonging to the transaction).
-
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
-
- my $res = $db->insert(
- 'users',
- {
- name => $user_name,
- status => 0,
- public_level => 0,
- email => $email,
- password => $password,
- registered_at => $now,
- last_seen => $now,
- },
- { returning => 'id' }
- );
- my $uid = $res->hash->{id};
-
- $db->insert(
- 'pending_registrations',
- {
- user_id => $uid,
- token => $token
- }
- );
-
- return $uid;
- }
- );
-
- $self->helper(
- 'flag_user_deletion' => sub {
- my ( $self, $uid ) = @_;
-
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
-
- $self->pg->db->update(
- 'users',
- { deletion_requested => $now },
- {
- id => $uid,
- }
- );
- }
- );
-
- $self->helper(
- 'unflag_user_deletion' => sub {
- my ( $self, $uid ) = @_;
-
- $self->pg->db->update(
- 'users',
- {
- deletion_requested => undef,
- },
- {
- id => $uid,
- }
- );
- }
- );
-
- $self->helper(
- 'set_user_password' => sub {
- my ( $self, $uid, $password ) = @_;
-
- $self->pg->db->update(
- 'users',
- { password => $password },
- { id => $uid }
- );
- }
- );
-
- $self->helper(
- 'check_if_user_name_exists' => sub {
- my ( $self, $user_name ) = @_;
-
- my $count = $self->pg->db->select(
- 'users',
- 'count(*) as count',
- { name => $user_name }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
- return 0;
- }
- );
-
- $self->helper(
- 'check_if_mail_is_blacklisted' => sub {
- my ( $self, $mail ) = @_;
-
- my $count = $self->pg->db->select(
- 'users',
- 'count(*) as count',
- {
- email => $mail,
- status => 0,
- }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
-
- $count = $self->pg->db->select(
- 'mail_blacklist',
- 'count(*) as count',
- {
- email => $mail,
- num_tries => { '>', 1 },
- }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
- return 0;
- }
- );
-
- $self->helper(
- 'delete_journey' => sub {
- my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_;
- my $uid = $self->current_user->{id};
-
- my @journeys = $self->get_user_travels(
- uid => $uid,
- journey_id => $journey_id
- );
- if ( @journeys == 0 ) {
- return 'Journey not found';
- }
- my $journey = $journeys[0];
-
- # Double-check (comparing both ID and action epoch) to make sure we
- # are really deleting the right journey and the user isn't just
- # playing around with POST requests.
- if ( $journey->{id} != $journey_id
- or $journey->{checkin_ts} != $checkin_epoch
- or $journey->{checkout_ts} != $checkout_epoch )
- {
- return 'Invalid journey data';
- }
-
- my $rows;
- eval {
- $rows = $self->pg->db->delete(
- 'journeys',
- {
- user_id => $uid,
- id => $journey_id,
- }
- )->rows;
- };
-
- if ($@) {
- $self->app->log->error("Delete($uid, $journey_id): $@");
- return 'DELETE failed: ' . $@;
- }
-
- if ( $rows == 1 ) {
- $self->invalidate_stats_cache(
- epoch_to_dt( $journey->{rt_dep_ts} ) );
- return undef;
- }
- return sprintf( 'Deleted %d rows, expected 1', $rows );
- }
- );
-
- $self->helper(
- 'get_journey_stats' => sub {
- my ( $self, %opt ) = @_;
-
- if ( $opt{cancelled} ) {
- $self->app->log->warning(
-'get_journey_stats called with illegal option cancelled => 1'
- );
- return {};
- }
-
- my $uid = $opt{uid} // $self->current_user->{id};
- my $year = $opt{year} // 0;
- my $month = $opt{month} // 0;
-
- # Assumption: If the stats cache contains an entry it is up-to-date.
- # -> Cache entries must be explicitly invalidated whenever the user
- # checks out of a train or manually edits/adds a journey.
-
- my $res = $self->pg->db->select(
- 'journey_stats',
- ['data'],
- {
- user_id => $uid,
- year => $year,
- month => $month
- }
- );
-
- my $res_h = $res->expand->hash;
-
- if ($res_h) {
- $res->finish;
- return $res_h->{data};
- }
-
- my $interval_start = DateTime->new(
- time_zone => 'Europe/Berlin',
- year => 2000,
- month => 1,
- day => 1,
- hour => 0,
- minute => 0,
- second => 0,
- );
-
- # I wonder if people will still be traveling by train in the year 3000
- my $interval_end = $interval_start->clone->add( years => 1000 );
-
- if ( $opt{year} and $opt{month} ) {
- $interval_start->set(
- year => $opt{year},
- month => $opt{month}
- );
- $interval_end = $interval_start->clone->add( months => 1 );
- }
- elsif ( $opt{year} ) {
- $interval_start->set( year => $opt{year} );
- $interval_end = $interval_start->clone->add( years => 1 );
- }
-
- my @journeys = $self->get_user_travels(
- uid => $uid,
- cancelled => $opt{cancelled} ? 1 : 0,
- verbose => 1,
- after => $interval_start,
- before => $interval_end
- );
- my $stats = $self->compute_journey_stats(@journeys);
-
- eval {
- $self->pg->db->insert(
- 'journey_stats',
- {
- user_id => $uid,
- year => $year,
- month => $month,
- data => JSON->new->encode($stats),
- }
- );
- };
- if ( my $err = $@ ) {
- if ( $err =~ m{duplicate key value violates unique constraint} )
- {
- # When a user opens the same history page several times in
- # short succession, there is a race condition where several
- # Mojolicious workers execute this helper, notice that there is
- # no up-to-date history, compute it, and insert it using the
- # statement above. This will lead to a uniqueness violation
- # in each successive insert. However, this is harmless, and
- # thus ignored.
- }
- else {
- # Otherwise we probably have a problem.
- die($@);
- }
- }
-
- return $stats;
- }
- );
-
- $self->helper(
- 'history_years' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id},
-
- my $res = $self->pg->db->select(
- 'journeys',
- 'distinct extract(year from real_departure) as year',
- { user_id => $uid },
- { order_by => { -asc => 'year' } }
- );
-
- my @ret;
- for my $row ( $res->hashes->each ) {
- push( @ret, [ $row->{year}, $row->{year} ] );
- }
- return @ret;
- }
- );
-
- $self->helper(
- 'history_months' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id},
-
- my $res = $self->pg->db->select(
- 'journeys',
- "distinct to_char(real_departure, 'YYYY.MM') as yearmonth",
- { user_id => $uid },
- { order_by => { -asc => 'yearmonth' } }
- );
-
- my @ret;
- for my $row ( $res->hashes->each ) {
- my ( $year, $month ) = split( qr{[.]}, $row->{yearmonth} );
- push( @ret, [ "${year}/${month}", "${month}.${year}" ] );
- }
- return @ret;
- }
- );
-
- $self->helper(
- 'route_diff' => sub {
- my ( $self, $train ) = @_;
- my @json_route;
- my @route = $train->route;
- my @sched_route = $train->sched_route;
-
- my $route_idx = 0;
- my $sched_idx = 0;
-
- while ( $route_idx <= $#route and $sched_idx <= $#sched_route ) {
- if ( $route[$route_idx] eq $sched_route[$sched_idx] ) {
- push( @json_route, [ $route[$route_idx], {}, undef ] );
- $route_idx++;
- $sched_idx++;
- }
-
- # this branch is inefficient, but won't be taken frequently
- elsif ( not( grep { $_ eq $route[$route_idx] } @sched_route ) )
- {
- push( @json_route,
- [ $route[$route_idx], {}, 'additional' ],
- );
- $route_idx++;
- }
- else {
- push( @json_route,
- [ $sched_route[$sched_idx], {}, 'cancelled' ],
- );
- $sched_idx++;
- }
- }
- while ( $route_idx <= $#route ) {
- push( @json_route, [ $route[$route_idx], {}, 'additional' ], );
- $route_idx++;
- }
- while ( $sched_idx <= $#sched_route ) {
- push( @json_route,
- [ $sched_route[$sched_idx], {}, 'cancelled' ],
- );
- $sched_idx++;
- }
- return @json_route;
- }
- );
-
- $self->helper(
- 'get_dbdb_station_p' => sub {
- my ( $self, $eva ) = @_;
-
- my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json";
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'utf-8', $tx->res->body );
-
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'has_wagonorder_p' => sub {
- my ( $self, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://lib.finalrewind.org/dbdb/has_wagonorder/${train_no}/${api_ts}";
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->get($url) ) {
- if ( $content eq 'y' ) {
- $promise->resolve;
- return $promise;
- }
- elsif ( $content eq 'n' ) {
- $promise->reject;
- return $promise;
- }
- }
-
- $self->ua->request_timeout(5)->head_p($url)->then(
- sub {
- my ($tx) = @_;
- if ( $tx->result->is_success ) {
- $cache->set( $url, 'y' );
- $promise->resolve;
- }
- else {
- $cache->set( $url, 'n' );
- $promise->resolve;
- }
- }
- )->catch(
- sub {
- $cache->set( $url, 'n' );
- $promise->reject;
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_wagonorder_p' => sub {
- my ( $self, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://www.apps-bahn.de/wr/wagenreihung/1.0/${train_no}/${api_ts}";
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'utf-8', $tx->res->body );
-
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_hafas_json_p' => sub {
- my ( $self, $url ) = @_;
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
-
- $body =~ s{^TSLs[.]sls = }{};
- $body =~ s{;$}{};
- $body =~ s{&#x0028;}{(}g;
- $body =~ s{&#x0029;}{)}g;
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->app->log->warning("get($url): $err");
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_hafas_xml_p' => sub {
- my ( $self, $url ) = @_;
-
- my $cache = $self->app->cache_iris_rt;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
- my $tree;
-
- my $traininfo = {
- station => {},
- messages => [],
- };
-
- # <SDay text="... &gt; ..."> is invalid HTML, but present in
- # regardless. As it is the last tag, we just throw it away.
- $body =~ s{<SDay [^>]*/>}{}s;
- eval { $tree = XML::LibXML->load_xml( string => $body ) };
- if ($@) {
- $self->app->log->warning("load_xml($url): $@");
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
- return;
- }
-
- for my $station ( $tree->findnodes('/Journey/St') ) {
- my $name = $station->getAttribute('name');
- my $adelay = $station->getAttribute('adelay');
- my $ddelay = $station->getAttribute('ddelay');
- $traininfo->{station}{$name} = {
- adelay => $adelay,
- ddelay => $ddelay,
- };
- }
-
- for my $message ( $tree->findnodes('/Journey/HIMMessage') )
- {
- my $header = $message->getAttribute('header');
- my $lead = $message->getAttribute('lead');
- my $display = $message->getAttribute('display');
- push(
- @{ $traininfo->{messages} },
- {
- header => $header,
- lead => $lead,
- display => $display
- }
- );
- }
-
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->app->log->warning("get($url): $err");
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
+ # This helper is only ever called from an IRIS context.
+ # HAFAS already has all relevant information.
$self->helper(
'add_route_timestamps' => sub {
- my ( $self, $uid, $train, $is_departure ) = @_;
+ my ( $self, $uid, $train, $is_departure, $update_polyline ) = @_;
$uid //= $self->current_user->{id};
my $db = $self->pg->db;
- my $journey = $db->select(
- 'in_transit_str',
- [ 'arr_eva', 'dep_eva', 'route' ],
- { user_id => $uid }
- )->expand->hash;
+ # TODO "with_timestamps" is misleading, there are more differences between in_transit and in_transit_str
+ # Here it's only needed because of dep_eva / arr_eva names
+ my $in_transit = $self->in_transit->get(
+ db => $db,
+ uid => $uid,
+ with_data => 1,
+ with_timestamps => 1
+ );
- if ( not $journey ) {
+ if ( not $in_transit ) {
return;
}
- my ($platform) = ( ( $train->platform // 0 ) =~ m{(\d+)} );
-
- my $route = $journey->{route};
-
- my $base
- = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json.vs_hap&start=yes&rt=1';
- my $date_yy = $train->start->strftime('%d.%m.%y');
- my $date_yyyy = $train->start->strftime('%d.%m.%Y');
- my $train_no = $train->type . ' ' . $train->train_no;
-
- my ( $trainlink, $route_data );
-
- $self->get_hafas_json_p(
- "${base}&date=${date_yy}&trainname=${train_no}")->then(
- sub {
- my ($trainsearch) = @_;
-
- # Fallback: Take first result
- $trainlink = $trainsearch->{suggestions}[0]{trainLink};
+ my $route = $in_transit->{route};
- # Try finding a result for the current date
- for
- my $suggestion ( @{ $trainsearch->{suggestions} // [] } )
- {
-
- # Drunken API, sail with care. Both date formats are used interchangeably
- if ( $suggestion->{depDate} eq $date_yy
- or $suggestion->{depDate} eq $date_yyyy )
- {
- # Train numbers are not unique, e.g. IC 149 refers both to the
- # InterCity service Amsterdam -> Berlin and to the InterCity service
- # Koebenhavns Lufthavn st -> Aarhus. One workaround is making
- # requests with the stationFilter=80 parameter. Checking the origin
- # station seems to be the more generic solution, so we do that
- # instead.
- if ( $suggestion->{dep} eq $train->origin ) {
- $trainlink = $suggestion->{trainLink};
- last;
- }
- }
- }
-
- if ( not $trainlink ) {
- $self->app->log->debug("trainlink not found");
- return Mojo::Promise->reject("trainlink not found");
- }
- my $base2
- = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';
- return $self->get_hafas_json_p(
-"${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_json.vs_hap"
- );
- }
- )->then(
+ $self->hafas->get_tripid_p( train => $train )->then(
sub {
- my ($traininfo) = @_;
- if ( not $traininfo or $traininfo->{error} ) {
- $self->app->log->debug("traininfo error");
- return Mojo::Promise->reject("traininfo error");
- }
- my $routeinfo
- = $traininfo->{suggestions}[0]{locations};
+ my ($trip_id) = @_;
- my $strp = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%y %H:%M',
- time_zone => 'Europe/Berlin',
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => { trip_id => $trip_id }
);
- $route_data = {};
-
- for my $station ( @{$routeinfo} ) {
- my $arr
- = $strp->parse_datetime(
- $station->{arrDate} . ' ' . $station->{arrTime} );
- my $dep
- = $strp->parse_datetime(
- $station->{depDate} . ' ' . $station->{depTime} );
- $route_data->{ $station->{name} } = {
- sched_arr => $arr ? $arr->epoch : 0,
- sched_dep => $dep ? $dep->epoch : 0,
- };
- }
-
- my $base2
- = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';
- return $self->get_hafas_xml_p(
- "${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3"
+ return $self->hafas->get_route_timestamps_p(
+ train => $train,
+ trip_id => $trip_id,
+ with_polyline => (
+ $update_polyline
+ or not $in_transit->{polyline}
+ ) ? 1 : 0,
);
}
)->then(
sub {
- my ($traininfo2) = @_;
+ my ( $route_data, $journey, $polyline ) = @_;
- for my $station ( keys %{$route_data} ) {
- for my $key (
- keys %{ $traininfo2->{station}{$station} // {} } )
+ for my $station ( @{$route} ) {
+ if ( $station->[0]
+ =~ m{^Betriebsstelle nicht bekannt (\d+)$} )
{
- $route_data->{$station}{$key}
- = $traininfo2->{station}{$station}{$key};
+ my $eva = $1;
+ if ( $route_data->{$eva} ) {
+ $station->[0] = $route_data->{$eva}{name};
+ $station->[1] = $route_data->{$eva}{eva};
+ }
}
- }
+ if ( my $sd = $route_data->{ $station->[0] } ) {
+ $station->[1] = $sd->{eva};
+ if ( $station->[2]{isAdditional} ) {
+ $sd->{isAdditional} = 1;
+ }
+ if ( $station->[2]{isCancelled} ) {
+ $sd->{isCancelled} = 1;
+ }
- for my $station ( @{$route} ) {
- $station->[1]
- = $route_data->{ $station->[0] };
+ # keep rt_dep / rt_arr if they are no longer present
+ my %old;
+ for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
+ $old{$k} = $station->[2]{$k};
+ }
+ $station->[2] = $sd;
+ if ( not $station->[2]{rt_arr} ) {
+ $station->[2]{rt_arr} = $old{rt_arr};
+ $station->[2]{arr_delay} = $old{arr_delay};
+ }
+ if ( not $station->[2]{rt_dep} ) {
+ $station->[2]{rt_dep} = $old{rt_dep};
+ $station->[2]{dep_delay} = $old{dep_delay};
+ }
+ }
}
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
+ my @messages;
+ for my $m ( $journey->messages ) {
+ if ( not $m->code ) {
+ push(
+ @messages,
+ {
+ header => $m->short,
+ lead => $m->text,
+ }
+ );
+ }
+ }
- $data->{delay_msg} = [ map { [ $_->[0]->epoch, $_->[1] ] }
- $train->delay_messages ];
- $data->{qos_msg} = [ map { [ $_->[0]->epoch, $_->[1] ] }
- $train->qos_messages ];
+ $self->in_transit->set_route_data(
+ uid => $uid,
+ db => $db,
+ route => $route,
+ delay_messages => [
+ map { [ $_->[0]->epoch, $_->[1] ] }
+ $train->delay_messages
+ ],
+ qos_messages => [
+ map { [ $_->[0]->epoch, $_->[1] ] }
+ $train->qos_messages
+ ],
+ him_messages => \@messages,
+ );
- $data->{him_msg} = $traininfo2->{messages};
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ old_id => $in_transit->{polyline_id},
+ );
+ }
- $db->update(
- 'in_transit',
- {
- route => JSON->new->encode($route),
- data => JSON->new->encode($data)
- },
- { user_id => $uid }
- );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->app->log->debug("add_route_timestamps: $err");
+ return;
}
)->wait;
if ( $train->sched_departure ) {
- $self->has_wagonorder_p( $train->sched_departure,
+ $self->dbdb->has_wagonorder_p( $train->sched_departure,
$train->train_no )->then(
sub {
- return $self->get_wagonorder_p( $train->sched_departure,
- $train->train_no );
+ my ($api) = @_;
+ return $self->dbdb->get_wagonorder_p( $api,
+ $train->sched_departure, $train->train_no );
}
)->then(
sub {
my ($wagonorder) = @_;
- my $res = $db->select(
- 'in_transit',
- [ 'data', 'user_data' ],
- { user_id => $uid }
- );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
- my $user_data = $res_h->{user_data} // {};
-
- if ($is_departure) {
- $data->{wagonorder_dep} = $wagonorder;
- if ( exists $user_data->{wagongroups} ) {
- $user_data->{wagongroups} = [];
- }
+ my $data = {};
+ my $user_data = {};
+
+ if ( $is_departure and not exists $wagonorder->{error} )
+ {
+ $data->{wagonorder_dep} = $wagonorder;
+ $user_data->{wagongroups} = [];
for my $group (
@{
$wagonorder->{data}{istformation}
@@ -2191,7 +1382,7 @@ sub startup {
push(
@wagons,
{
- id => $wagon->{fahrzeugnummer},
+ id => $wagon->{fahrzeugnummer},
number =>
$wagon->{wagenordnungsnummer},
type => $wagon->{fahrzeugtyp},
@@ -2210,449 +1401,99 @@ sub startup {
wagons => [@wagons],
}
);
- }
- $db->update(
- 'in_transit',
+ if ( $group->{fahrzeuggruppebezeichnung}
+ and $group->{fahrzeuggruppebezeichnung} eq
+ 'ICE0304' )
{
- data => JSON->new->encode($data),
- user_data => JSON->new->encode($user_data)
- },
- { user_id => $uid }
+ $data->{wagonorder_pride} = 1;
+ }
+ }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
+ );
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data => $user_data
);
}
- else {
+ elsif ( not $is_departure
+ and not exists $wagonorder->{error} )
+ {
$data->{wagonorder_arr} = $wagonorder;
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
}
+ return;
+ }
+ )->catch(
+ sub {
+ # no wagonorder? no problem.
+ return;
}
)->wait;
}
if ($is_departure) {
- $self->get_dbdb_station_p( $journey->{dep_eva} )->then(
+ $self->dbdb->get_stationinfo_p( $in_transit->{dep_eva} )->then(
sub {
my ($station_info) = @_;
+ my $data = { stationinfo_dep => $station_info };
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
-
- $data->{stationinfo_dep} = $station_info;
-
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
+ return;
+ }
+ )->catch(
+ sub {
+ # no stationinfo? no problem.
+ return;
}
)->wait;
}
- if ( $journey->{arr_eva} and not $is_departure ) {
- $self->get_dbdb_station_p( $journey->{arr_eva} )->then(
+ if ( $in_transit->{arr_eva} and not $is_departure ) {
+ $self->dbdb->get_stationinfo_p( $in_transit->{arr_eva} )->then(
sub {
my ($station_info) = @_;
+ my $data = { stationinfo_arr => $station_info };
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
-
- $data->{stationinfo_arr} = $station_info;
-
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
+ return;
}
- )->wait;
- }
- }
- );
-
- $self->helper(
- 'get_oldest_journey_ts' => sub {
- my ($self) = @_;
-
- my $res_h = $self->pg->db->select(
- 'journeys_str',
- ['sched_dep_ts'],
- {
- user_id => $self->current_user->{id},
- },
- {
- limit => 1,
- order_by => {
- -asc => 'real_dep_ts',
- },
- }
- )->hash;
-
- if ($res_h) {
- return epoch_to_dt( $res_h->{sched_dep_ts} );
- }
- return undef;
- }
- );
-
- $self->helper(
- 'get_latest_dest_id' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} // $self->current_user->{id};
- my $db = $opt{db} // $self->pg->db;
-
- my $journey = $db->select( 'in_transit', ['checkout_station_id'],
- { user_id => $uid } )->hash;
- if ( not $journey ) {
- $journey = $db->select(
- 'journeys',
- ['checkout_station_id'],
- {
- user_id => $uid,
- cancelled => 0
- },
- {
- limit => 1,
- order_by => { -desc => 'real_departure' }
- }
- )->hash;
- }
-
- if ( not $journey ) {
- return;
- }
-
- return $journey->{checkout_station_id};
- }
- );
-
- $self->helper(
- 'get_connection_targets' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} //= $self->current_user->{id};
- 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} // $self->get_latest_dest_id(%opt);
-
- if ( not $dest_id ) {
- return;
- }
-
- my $res = $db->query(
- qq{
- select
- count(checkout_station_id) as count,
- checkout_station_id as dest
- from journeys
- where user_id = ?
- and checkin_station_id = ?
- and real_departure > ?
- group by checkout_station_id
- order by count desc;
- },
- $uid,
- $dest_id,
- $threshold
- );
- my @destinations
- = $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )->each;
- @destinations
- = grep { $self->app->station_by_eva->{$_} } @destinations;
- @destinations
- = map { $self->app->station_by_eva->{$_}->[1] } @destinations;
- return @destinations;
- }
- );
-
- $self->helper(
- 'get_connecting_trains' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} //= $self->current_user->{id};
- my $use_history = $self->account_use_history($uid);
-
- my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
-
- if ( $opt{eva} ) {
- if ( $use_history & 0x01 ) {
- $eva = $opt{eva};
- }
- }
- else {
- if ( $use_history & 0x02 ) {
- my $status = $self->get_user_status;
- $eva = $status->{arr_eva};
- $exclude_via = $status->{dep_name};
- $exclude_train_id = $status->{train_id};
- $exclude_before = $status->{real_arrival}->epoch;
- }
- }
-
- if ( not $eva ) {
- return;
- }
-
- my @destinations = $self->get_connection_targets(%opt);
-
- if ($exclude_via) {
- @destinations = grep { $_ ne $exclude_via } @destinations;
- }
-
- if ( not @destinations ) {
- return;
- }
-
- my $stationboard = $self->get_departures( $eva, 0, 40, 1 );
- if ( $stationboard->{errstr} ) {
- return;
- }
- @{ $stationboard->{results} } = map { $_->[0] }
- sort { $a->[1] <=> $b->[1] }
- map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
- @{ $stationboard->{results} };
- my @results;
- my @cancellations;
- my %via_count = map { $_ => 0 } @destinations;
- for my $train ( @{ $stationboard->{results} } ) {
- if ( not $train->departure ) {
- next;
- }
- if ( $exclude_before
- and $train->departure->epoch < $exclude_before )
- {
- next;
- }
- if ( $exclude_train_id
- and $train->train_id eq $exclude_train_id )
- {
- next;
- }
-
- # In general, this function is meant to return feasible
- # connections. However, cancelled connections may also be of
- # interest and are also useful for logging cancellations.
- # To satisfy both demands with (hopefully) little confusion and
- # UI clutter, this function returns two concatenated arrays:
- # actual connections (ordered by actual departure time) followed
- # by cancelled connections (ordered by scheduled departure time).
- # This is easiest to achieve in two separate loops.
- #
- # Note that a cancelled train may still have a matching destination
- # in its route_post, e.g. if it leaves out $eva due to
- # unscheduled route changes but continues on schedule afterwards
- # -- so it is only cancelled at $eva, not on the remainder of
- # the route. Also note that this specific case is not yet handled
- # properly by the cancellation logic etc.
-
- if ( $train->departure_is_cancelled ) {
- my @via
- = ( $train->sched_route_post, $train->sched_route_end );
- for my $dest (@destinations) {
- if ( List::Util::any { $_ eq $dest } @via ) {
- push( @cancellations, [ $train, $dest ] );
- next;
- }
- }
- }
- else {
- my @via = ( $train->route_post, $train->route_end );
- for my $dest (@destinations) {
- if ( $via_count{$dest} < 2
- and List::Util::any { $_ eq $dest } @via )
- {
- push( @results, [ $train, $dest ] );
- $via_count{$dest}++;
- next;
- }
- }
- }
- }
-
- @results = map { $_->[0] }
- sort { $a->[1] <=> $b->[1] }
- map {
- [
- $_,
- $_->[0]->departure->epoch // $_->[0]->sched_departure->epoch
- ]
- } @results;
- @cancellations = map { $_->[0] }
- sort { $a->[1] <=> $b->[1] }
- map { [ $_, $_->[0]->sched_departure->epoch ] } @cancellations;
-
- return ( @results, @cancellations );
- }
- );
-
- $self->helper(
- 'account_use_history' => sub {
- my ( $self, $uid, $value ) = @_;
-
- if ($value) {
- $self->pg->db->update(
- 'users',
- { use_history => $value },
- { id => $uid }
- );
- }
- else {
- return $self->pg->db->select( 'users', ['use_history'],
- { id => $uid } )->hash->{use_history};
- }
- }
- );
-
- $self->helper(
- 'get_user_travels' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} || $self->current_user->{id};
-
- # If get_user_travels is called from inside a transaction, db
- # specifies the database handle performing the transaction.
- # Otherwise, we grab a fresh one.
- my $db = $opt{db} // $self->pg->db;
-
- my %where = (
- user_id => $uid,
- cancelled => 0
- );
- my %order = (
- order_by => {
- -desc => 'real_dep_ts',
- }
- );
-
- if ( $opt{cancelled} ) {
- $where{cancelled} = 1;
- }
-
- if ( $opt{limit} ) {
- $order{limit} = $opt{limit};
- }
-
- if ( $opt{journey_id} ) {
- $where{journey_id} = $opt{journey_id};
- delete $where{cancelled};
- }
- elsif ( $opt{after} and $opt{before} ) {
- $where{real_dep_ts} = {
- -between => [ $opt{after}->epoch, $opt{before}->epoch, ] };
- }
-
- my @travels;
-
- my $res = $db->select( 'journeys_str', '*', \%where, \%order );
-
- for my $entry ( $res->expand->hashes->each ) {
-
- my $ref = {
- id => $entry->{journey_id},
- type => $entry->{train_type},
- line => $entry->{train_line},
- no => $entry->{train_no},
- from_eva => $entry->{dep_eva},
- checkin_ts => $entry->{checkin_ts},
- sched_dep_ts => $entry->{sched_dep_ts},
- rt_dep_ts => $entry->{real_dep_ts},
- to_eva => $entry->{arr_eva},
- checkout_ts => $entry->{checkout_ts},
- sched_arr_ts => $entry->{sched_arr_ts},
- rt_arr_ts => $entry->{real_arr_ts},
- messages => $entry->{messages},
- route => $entry->{route},
- edited => $entry->{edited},
- user_data => $entry->{user_data},
- };
-
- if ( my $station
- = $self->app->station_by_eva->{ $ref->{from_eva} } )
- {
- $ref->{from_ds100} = $station->[0];
- $ref->{from_name} = $station->[1];
- }
- if ( my $station
- = $self->app->station_by_eva->{ $ref->{to_eva} } )
- {
- $ref->{to_ds100} = $station->[0];
- $ref->{to_name} = $station->[1];
- }
-
- if ( $opt{with_datetime} ) {
- $ref->{checkin} = epoch_to_dt( $ref->{checkin_ts} );
- $ref->{sched_departure}
- = epoch_to_dt( $ref->{sched_dep_ts} );
- $ref->{rt_departure} = epoch_to_dt( $ref->{rt_dep_ts} );
- $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 ( $opt{verbose} ) {
- $ref->{cancelled} = $entry->{cancelled};
- my @parsed_messages;
- for my $message ( @{ $ref->{messages} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
+ )->catch(
+ sub {
+ # no stationinfo? no problem.
+ return;
}
- $ref->{messages} = [ reverse @parsed_messages ];
- $ref->{sched_duration}
- = $ref->{sched_arr_ts}
- ? $ref->{sched_arr_ts} - $ref->{sched_dep_ts}
- : undef;
- $ref->{rt_duration}
- = $ref->{rt_arr_ts}
- ? $ref->{rt_arr_ts} - $ref->{rt_dep_ts}
- : undef;
- my ( $km_route, $km_beeline, $skip )
- = $self->get_travel_distance( $ref->{from_name},
- $ref->{to_name}, $ref->{route} );
- $ref->{km_route} = $km_route;
- $ref->{skip_route} = $skip;
- $ref->{km_beeline} = $km_beeline;
- $ref->{skip_beeline} = $skip;
- my $kmh_divisor
- = ( $ref->{rt_duration} // $ref->{sched_duration}
- // 999999 ) / 3600;
- $ref->{kmh_route}
- = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1;
- $ref->{kmh_beeline}
- = $kmh_divisor
- ? $ref->{km_beeline} / $kmh_divisor
- : -1;
- }
-
- push( @travels, $ref );
+ )->wait;
}
-
- return @travels;
}
);
$self->helper(
- 'get_journey' => sub {
- my ( $self, %opt ) = @_;
-
- $opt{cancelled} = 'any';
- my @journeys = $self->get_user_travels(%opt);
- if ( @journeys == 0 ) {
- return undef;
- }
-
- return $journeys[0];
+ 'resolve_sb_template' => sub {
+ my ( $self, $template, %opt ) = @_;
+ my $ret = $template;
+ my $name = $opt{name} =~ s{/}{%2F}gr;
+ $ret =~ s{[{]eva[}]}{$opt{eva}}g;
+ $ret =~ s{[{]name[}]}{$name}g;
+ $ret =~ s{[{]tt[}]}{$opt{tt}}g;
+ $ret =~ s{[{]tn[}]}{$opt{tn}}g;
+ $ret =~ s{[{]id[}]}{$opt{id}}g;
+ return $ret;
}
);
@@ -2720,21 +1561,17 @@ sub startup {
for my $station ( @{ $journey->{route_after} } ) {
my $station_desc = $station->[0];
- if ( $station->[1]{rt_arr} ) {
- $station_desc .= $station->[1]{sched_arr}->strftime(';%s');
- $station_desc .= $station->[1]{rt_arr}->strftime(';%s');
- if ( $station->[1]{rt_dep} ) {
- $station_desc
- .= $station->[1]{sched_dep}->strftime(';%s');
- $station_desc .= $station->[1]{rt_dep}->strftime(';%s');
- }
- else {
- $station_desc .= ';0;0';
- }
- }
- else {
- $station_desc .= ';0;0;0;0';
- }
+
+ my $sa = $station->[2]{sched_arr};
+ my $ra = $station->[2]{rt_arr} || $station->[2]{sched_arr};
+ my $sd = $station->[2]{sched_dep};
+ my $rd = $station->[2]{rt_dep} || $station->[2]{sched_dep};
+
+ $station_desc .= $sa ? $sa->strftime(';%s') : ';0';
+ $station_desc .= $ra ? $ra->strftime(';%s') : ';0';
+ $station_desc .= $sd ? $sd->strftime(';%s') : ';0';
+ $station_desc .= $rd ? $rd->strftime(';%s') : ';0';
+
push( @route, $station_desc );
}
@@ -2744,166 +1581,87 @@ sub startup {
$self->helper(
'get_user_status' => sub {
- my ( $self, $uid ) = @_;
+ my ( $self, $uid, $db ) = @_;
$uid //= $self->current_user->{id};
+ $db //= $self->pg->db;
- my $db = $self->pg->db;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $epoch = $now->epoch;
- my $in_transit
- = $db->select( 'in_transit_str', '*', { user_id => $uid } )
- ->expand->hash;
+ my $in_transit = $self->in_transit->get(
+ uid => $uid,
+ db => $db,
+ with_data => 1,
+ with_timestamps => 1,
+ with_visibility => 1,
+ postprocess => 1,
+ );
if ($in_transit) {
+ my $ret = $in_transit;
- if ( my $station
- = $self->app->station_by_eva->{ $in_transit->{dep_eva} } )
- {
- $in_transit->{dep_ds100} = $station->[0];
- $in_transit->{dep_name} = $station->[1];
- }
- if ( $in_transit->{arr_eva}
- and my $station
- = $self->app->station_by_eva->{ $in_transit->{arr_eva} } )
+ my $traewelling = $self->traewelling->get(
+ uid => $uid,
+ db => $db
+ );
+ if ( $traewelling->{latest_run}
+ >= epoch_to_dt( $in_transit->{checkin_ts} ) )
{
- $in_transit->{arr_ds100} = $station->[0];
- $in_transit->{arr_name} = $station->[1];
- }
-
- my @route = @{ $in_transit->{route} // [] };
- my @route_after;
- my $dep_info;
- my $stop_before_dest;
- my $is_after = 0;
- for my $station (@route) {
-
- if ( $in_transit->{arr_name}
- and @route_after
- and $station->[0] eq $in_transit->{arr_name} )
+ $ret->{traewelling} = $traewelling;
+ if ( @{ $traewelling->{data}{log} // [] }
+ and ( my $log_entry = $traewelling->{data}{log}[0] ) )
{
- $stop_before_dest = $route_after[-1][0];
- }
- if ($is_after) {
- push( @route_after, $station );
- }
- if ( $in_transit->{dep_name}
- and $station->[0] eq $in_transit->{dep_name} )
- {
- $is_after = 1;
- if ( @{$station} > 1 ) {
- $dep_info = $station->[1];
+ if ( $log_entry->[2] ) {
+ $ret->{traewelling_status} = $log_entry->[2];
+ $ret->{traewelling_url}
+ = 'https://traewelling.de/status/'
+ . $log_entry->[2];
}
+ $ret->{traewelling_log_latest} = $log_entry->[1];
}
}
- my $stop_after_dep = @route_after ? $route_after[0][0] : undef;
-
- my $ts = $in_transit->{checkout_ts}
- // $in_transit->{checkin_ts};
- my $action_time = epoch_to_dt($ts);
-
- my $ret = {
- checked_in => !$in_transit->{cancelled},
- cancelled => $in_transit->{cancelled},
- timestamp => $action_time,
- timestamp_delta => $now->epoch - $action_time->epoch,
- train_type => $in_transit->{train_type},
- train_line => $in_transit->{train_line},
- train_no => $in_transit->{train_no},
- train_id => $in_transit->{train_id},
- boarding_countdown => -1,
- sched_departure =>
- epoch_to_dt( $in_transit->{sched_dep_ts} ),
- real_departure => epoch_to_dt( $in_transit->{real_dep_ts} ),
- dep_ds100 => $in_transit->{dep_ds100},
- dep_eva => $in_transit->{dep_eva},
- dep_name => $in_transit->{dep_name},
- dep_platform => $in_transit->{dep_platform},
- sched_arrival => epoch_to_dt( $in_transit->{sched_arr_ts} ),
- real_arrival => epoch_to_dt( $in_transit->{real_arr_ts} ),
- arr_ds100 => $in_transit->{arr_ds100},
- arr_eva => $in_transit->{arr_eva},
- arr_name => $in_transit->{arr_name},
- arr_platform => $in_transit->{arr_platform},
- route_after => \@route_after,
- messages => $in_transit->{messages},
- extra_data => $in_transit->{data},
- comment => $in_transit->{user_data}{comment},
- };
-
- my @parsed_messages;
- for my $message ( @{ $ret->{messages} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
- }
- $ret->{messages} = [ reverse @parsed_messages ];
- @parsed_messages = ();
- for my $message ( @{ $ret->{extra_data}{qos_msg} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
- }
- $ret->{extra_data}{qos_msg} = [@parsed_messages];
-
- if ( $dep_info and $dep_info->{sched_arr} ) {
- $dep_info->{sched_arr}
- = epoch_to_dt( $dep_info->{sched_arr} );
- $dep_info->{rt_arr} = $dep_info->{sched_arr}->clone;
- if ( $dep_info->{adelay}
- and $dep_info->{adelay} =~ m{^\d+$} )
+ my $stop_after_dep
+ = scalar @{ $ret->{route_after} }
+ ? $ret->{route_after}[0][0]
+ : undef;
+ my $stop_before_dest;
+ for my $i ( 1 .. $#{ $ret->{route_after} } ) {
+ if ( $ret->{arr_name}
+ and $ret->{route_after}[$i][0] eq $ret->{arr_name} )
{
- $dep_info->{rt_arr}
- ->add( minutes => $dep_info->{adelay} );
+ $stop_before_dest = $ret->{route_after}[ $i - 1 ][0];
+ last;
}
- $dep_info->{rt_arr_countdown} = $ret->{boarding_countdown}
- = $dep_info->{rt_arr}->epoch - $epoch;
}
- for my $station (@route_after) {
- if ( @{$station} > 1 ) {
-
- # Note: $station->[1]{sched_arr} may already have been
- # converted to a DateTime object in $station->[1] is
- # $dep_info. This can happen when a station is present
- # several times in a train's route, e.g. for Frankfurt
- # Flughafen in some nightly connections.
- my $times = $station->[1];
- if ( $times->{sched_arr}
- and ref( $times->{sched_arr} ) ne 'DateTime' )
- {
- $times->{sched_arr}
- = epoch_to_dt( $times->{sched_arr} );
- $times->{rt_arr} = $times->{sched_arr}->clone;
- if ( $times->{adelay}
- and $times->{adelay} =~ m{^\d+$} )
- {
- $times->{rt_arr}
- ->add( minutes => $times->{adelay} );
- }
- $times->{rt_arr_countdown}
- = $times->{rt_arr}->epoch - $epoch;
- }
- if ( $times->{sched_dep}
- and ref( $times->{sched_dep} ) ne 'DateTime' )
- {
- $times->{sched_dep}
- = epoch_to_dt( $times->{sched_dep} );
- $times->{rt_dep} = $times->{sched_dep}->clone;
- if ( $times->{ddelay}
- and $times->{ddelay} =~ m{^\d+$} )
- {
- $times->{rt_dep}
- ->add( minutes => $times->{ddelay} );
- }
- $times->{rt_dep_countdown}
- = $times->{rt_dep}->epoch - $epoch;
- }
- }
+ my ($dep_platform_number)
+ = ( ( $ret->{dep_platform} // 0 ) =~ m{(\d+)} );
+ if ( $dep_platform_number
+ and
+ exists $ret->{data}{stationinfo_dep}{$dep_platform_number} )
+ {
+ $ret->{dep_direction} = $self->stationinfo_to_direction(
+ $ret->{data}{stationinfo_dep}{$dep_platform_number},
+ $ret->{data}{wagonorder_dep},
+ undef, $stop_after_dep
+ );
}
- $ret->{departure_countdown}
- = $ret->{real_departure}->epoch - $now->epoch;
+ my ($arr_platform_number)
+ = ( ( $ret->{arr_platform} // 0 ) =~ m{(\d+)} );
+ if ( $arr_platform_number
+ and
+ exists $ret->{data}{stationinfo_arr}{$arr_platform_number} )
+ {
+ $ret->{arr_direction} = $self->stationinfo_to_direction(
+ $ret->{data}{stationinfo_arr}{$arr_platform_number},
+ $ret->{data}{wagonorder_arr},
+ $stop_before_dest,
+ undef
+ );
+ }
if ( $ret->{departure_countdown} > 0
and $in_transit->{data}{wagonorder_dep} )
@@ -2915,7 +1673,6 @@ sub startup {
from_json => $in_transit->{data}{wagonorder_dep} );
};
if ( $wr
- and $wr->sections
and $wr->wagons
and defined $wr->direction )
{
@@ -2923,97 +1680,57 @@ sub startup {
}
}
- if ( $in_transit->{real_arr_ts} ) {
- $ret->{arrival_countdown}
- = $ret->{real_arrival}->epoch - $now->epoch;
- $ret->{journey_duration}
- = $ret->{real_arrival}->epoch
- - $ret->{real_departure}->epoch;
- $ret->{journey_completion}
- = $ret->{journey_duration}
- ? 1
- - ( $ret->{arrival_countdown} / $ret->{journey_duration} )
- : 1;
- if ( $ret->{journey_completion} > 1 ) {
- $ret->{journey_completion} = 1;
- }
- elsif ( $ret->{journey_completion} < 0 ) {
- $ret->{journey_completion} = 0;
- }
-
- my ($dep_platform_number)
- = ( ( $ret->{dep_platform} // 0 ) =~ m{(\d+)} );
- if ( $dep_platform_number
- and exists $in_transit->{data}{stationinfo_dep}
- {$dep_platform_number} )
- {
- $ret->{dep_direction}
- = $self->stationinfo_to_direction(
- $in_transit->{data}{stationinfo_dep}
- {$dep_platform_number},
- $in_transit->{data}{wagonorder_dep},
- undef,
- $stop_after_dep
- );
- }
-
- my ($arr_platform_number)
- = ( ( $ret->{arr_platform} // 0 ) =~ m{(\d+)} );
- if ( $arr_platform_number
- and exists $in_transit->{data}{stationinfo_arr}
- {$arr_platform_number} )
- {
- $ret->{arr_direction}
- = $self->stationinfo_to_direction(
- $in_transit->{data}{stationinfo_arr}
- {$arr_platform_number},
- $in_transit->{data}{wagonorder_arr},
- $stop_before_dest,
- undef
- );
- }
-
- }
- else {
- $ret->{arrival_countdown} = undef;
- $ret->{journey_duration} = undef;
- $ret->{journey_completion} = undef;
- }
-
return $ret;
}
- my $latest = $db->select(
- 'journeys_str',
- '*',
+ my ( $latest, $latest_cancellation ) = $self->journeys->get_latest(
+ uid => $uid,
+ db => $db,
+ );
+
+ if ( $latest_cancellation and $latest_cancellation->{cancelled} ) {
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{dep_eva}
+ )
+ )
{
- user_id => $uid,
- cancelled => 0
- },
+ $latest_cancellation->{dep_ds100} = $station->{ds100};
+ $latest_cancellation->{dep_name} = $station->{name};
+ }
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{arr_eva}
+ )
+ )
{
- order_by => { -desc => 'journey_id' },
- limit => 1
+ $latest_cancellation->{arr_ds100} = $station->{ds100};
+ $latest_cancellation->{arr_name} = $station->{name};
}
- )->expand->hash;
+ }
+ else {
+ $latest_cancellation = undef;
+ }
if ($latest) {
my $ts = $latest->{checkout_ts};
my $action_time = epoch_to_dt($ts);
if ( my $station
- = $self->app->station_by_eva->{ $latest->{dep_eva} } )
+ = $self->stations->get_by_eva( $latest->{dep_eva} ) )
{
- $latest->{dep_ds100} = $station->[0];
- $latest->{dep_name} = $station->[1];
+ $latest->{dep_ds100} = $station->{ds100};
+ $latest->{dep_name} = $station->{name};
}
if ( my $station
- = $self->app->station_by_eva->{ $latest->{arr_eva} } )
+ = $self->stations->get_by_eva( $latest->{arr_eva} ) )
{
- $latest->{arr_ds100} = $station->[0];
- $latest->{arr_name} = $station->[1];
+ $latest->{arr_ds100} = $station->{ds100};
+ $latest->{arr_name} = $station->{name};
}
return {
checked_in => 0,
cancelled => 0,
+ cancellation => $latest_cancellation,
journey_id => $latest->{journey_id},
timestamp => $action_time,
timestamp_delta => $now->epoch - $action_time->epoch,
@@ -3026,20 +1743,30 @@ sub startup {
dep_ds100 => $latest->{dep_ds100},
dep_eva => $latest->{dep_eva},
dep_name => $latest->{dep_name},
+ dep_lat => $latest->{dep_lat},
+ dep_lon => $latest->{dep_lon},
dep_platform => $latest->{dep_platform},
sched_arrival => epoch_to_dt( $latest->{sched_arr_ts} ),
real_arrival => epoch_to_dt( $latest->{real_arr_ts} ),
arr_ds100 => $latest->{arr_ds100},
arr_eva => $latest->{arr_eva},
arr_name => $latest->{arr_name},
+ arr_lat => $latest->{arr_lat},
+ arr_lon => $latest->{arr_lon},
arr_platform => $latest->{arr_platform},
comment => $latest->{user_data}{comment},
+ visibility => $latest->{visibility},
+ visibility_str => $latest->{visibility_str},
+ effective_visibility => $latest->{effective_visibility},
+ effective_visibility_str =>
+ $latest->{effective_visibility_str},
};
}
return {
checked_in => 0,
cancelled => 0,
+ cancellation => $latest_cancellation,
no_journeys_yet => 1,
timestamp => epoch_to_dt(0),
timestamp_delta => $now->epoch,
@@ -3049,10 +1776,11 @@ sub startup {
$self->helper(
'get_user_status_json_v1' => sub {
- my ( $self, $uid ) = @_;
- my $status = $self->get_user_status($uid);
-
- # TODO simplify lon/lat (can be returned from get_user_status)
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $privacy = $opt{privacy}
+ // $self->users->get_privacy_by( uid => $uid );
+ my $status = $opt{status} // $self->get_user_status($uid);
my $ret = {
deprecated => \0,
@@ -3060,57 +1788,82 @@ sub startup {
$status->{checked_in}
or $status->{cancelled}
) ? \1 : \0,
+ comment => $status->{comment},
fromStation => {
ds100 => $status->{dep_ds100},
name => $status->{dep_name},
uic => $status->{dep_eva},
- longitude => undef,
- latitude => undef,
- scheduledTime => $status->{sched_departure}->epoch || undef,
- realTime => $status->{real_departure}->epoch || undef,
+ longitude => $status->{dep_lon},
+ latitude => $status->{dep_lat},
+ scheduledTime => $status->{sched_departure}
+ ? $status->{sched_departure}->epoch
+ : undef,
+ realTime => $status->{real_departure}
+ ? $status->{real_departure}->epoch
+ : undef,
},
toStation => {
ds100 => $status->{arr_ds100},
name => $status->{arr_name},
uic => $status->{arr_eva},
- longitude => undef,
- latitude => undef,
- scheduledTime => $status->{sched_arrival}->epoch || undef,
- realTime => $status->{real_arrival}->epoch || undef,
+ longitude => $status->{arr_lon},
+ latitude => $status->{arr_lat},
+ scheduledTime => $status->{sched_arrival}
+ ? $status->{sched_arrival}->epoch
+ : undef,
+ realTime => $status->{real_arrival}
+ ? $status->{real_arrival}->epoch
+ : undef,
},
train => {
- type => $status->{train_type},
- line => $status->{train_line},
- no => $status->{train_no},
- id => $status->{train_id},
+ type => $status->{train_type},
+ line => $status->{train_line},
+ no => $status->{train_no},
+ id => $status->{train_id},
+ hafasId => $status->{extra_data}{trip_id},
},
- actionTime => $status->{timestamp}->epoch,
+ intermediateStops => [],
+ visibility => {
+ level => $status->{effective_visibility},
+ desc => $status->{effective_visibility_str},
+ }
};
- if ( $status->{dep_eva} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{dep_eva} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{fromStation}{longitude},
- $ret->{fromStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ if ( $opt{public} ) {
+ if ( not $privacy->{comments_visible} ) {
+ delete $ret->{comment};
}
}
+ else {
+ $ret->{actionTime}
+ = $status->{timestamp}
+ ? $status->{timestamp}->epoch
+ : undef;
+ }
- if ( $status->{arr_ds100} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{arr_ds100} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{toStation}{longitude},
- $ret->{toStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ for my $stop ( @{ $status->{route_after} // [] } ) {
+ if ( $status->{arr_name} and $stop->[0] eq $status->{arr_name} )
+ {
+ last;
}
+ push(
+ @{ $ret->{intermediateStops} },
+ {
+ name => $stop->[0],
+ scheduledArrival => $stop->[2]{sched_arr}
+ ? $stop->[2]{sched_arr}->epoch
+ : undef,
+ realArrival => $stop->[2]{rt_arr}
+ ? $stop->[2]{rt_arr}->epoch
+ : undef,
+ scheduledDeparture => $stop->[2]{sched_dep}
+ ? $stop->[2]{sched_dep}->epoch
+ : undef,
+ realDeparture => $stop->[2]{rt_dep}
+ ? $stop->[2]{rt_dep}->epoch
+ : undef,
+ }
+ );
}
return $ret;
@@ -3118,140 +1871,464 @@ sub startup {
);
$self->helper(
- 'get_travel_distance' => sub {
- my ( $self, $from, $to, $route_ref ) = @_;
-
- my $distance_intermediate = 0;
- my $distance_beeline = 0;
- my $skipped = 0;
- my $geo = Geo::Distance->new();
- my @stations = map { $_->[0] } @{$route_ref};
- my @route = after_incl { $_ eq $from } @stations;
- @route = before_incl { $_ eq $to } @route;
+ 'traewelling_to_travelynx_p' => sub {
+ my ( $self, %opt ) = @_;
+ my $traewelling = $opt{traewelling};
+ my $user_data = $opt{user_data};
+ my $uid = $user_data->{user_id};
- if ( @route < 2 ) {
+ my $promise = Mojo::Promise->new;
- # I AM ERROR
- return ( 0, 0 );
+ if ( not $traewelling->{checkin}
+ or $self->now->epoch - $traewelling->{checkin}->epoch > 900 )
+ {
+ $self->log->debug("... not checked in");
+ return $promise->resolve;
}
-
- my $prev_station = get_station( shift @route );
- if ( not $prev_station ) {
- return ( 0, 0 );
+ if ( $traewelling->{status_id}
+ and $user_data->{data}{latest_pull_status_id}
+ and $traewelling->{status_id}
+ == $user_data->{data}{latest_pull_status_id} )
+ {
+ $self->log->debug("... already handled");
+ return $promise->resolve;
+ }
+ $self->log->debug(
+"... checked in : $traewelling->{dep_name} $traewelling->{dep_eva} -> $traewelling->{arr_name} $traewelling->{arr_eva}"
+ );
+ my $user_status = $self->get_user_status($uid);
+ if ( $user_status->{checked_in} ) {
+ $self->log->debug(
+ "... also checked in via travelynx. aborting.");
+ return $promise->resolve;
}
- # Geo-coordinates for stations outside Germany are not available
- # at the moment. When calculating distance with intermediate stops,
- # these are simply left out (as if they were not part of the route).
- # For beeline distance calculation, we use the route's first and last
- # station with known geo-coordinates.
- my $from_station_beeline;
- my $to_station_beeline;
-
- for my $station_name (@route) {
- if ( my $station = get_station($station_name) ) {
- if ( not $from_station_beeline and $#{$prev_station} >= 4 )
- {
- $from_station_beeline = $prev_station;
- }
- if ( $#{$station} >= 4 ) {
- $to_station_beeline = $station;
+ if ( $traewelling->{category}
+ !~ m{^ (?: national .* | regional .* | suburban ) $ }x )
+ {
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->checkin_p(
+ station => $traewelling->{dep_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ )->then(
+ sub {
+ $self->log->debug("... handled origin");
+ return $self->checkout_p(
+ station => $traewelling->{arr_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ );
}
- if ( $#{$prev_station} >= 4 and $#{$station} >= 4 ) {
- $distance_intermediate
- += $geo->distance( 'kilometer', $prev_station->[3],
- $prev_station->[4], $station->[3], $station->[4] );
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
+ if ( $traewelling->{message} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data =>
+ { comment => $traewelling->{message} }
+ );
+ }
+ $self->traewelling->log(
+ uid => $uid,
+ db => $db,
+ message =>
+"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
+ status_id => $traewelling->{status_id},
+ );
+ $self->traewelling->set_latest_pull_status_id(
+ uid => $uid,
+ status_id => $traewelling->{status_id},
+ db => $db
+ );
+
+ $tx->commit;
+ $promise->resolve;
+ return;
}
- else {
- $skipped++;
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ $promise->resolve;
+ return;
}
- $prev_station = $station;
- }
+ )->wait;
+ return $promise;
}
- if ( $from_station_beeline and $to_station_beeline ) {
- $distance_beeline = $geo->distance(
- 'kilometer', $from_station_beeline->[3],
- $from_station_beeline->[4], $to_station_beeline->[3],
- $to_station_beeline->[4]
- );
- }
+ $self->iris->get_departures_p(
+ station => $traewelling->{dep_eva},
+ lookbehind => 60,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($dep) = @_;
+ my ( $train_ref, $train_id );
+
+ if ( $dep->{errstr} ) {
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+ status_id => $traewelling->{status_id},
+ is_error => 1,
+ );
+ $promise->resolve;
+ return;
+ }
+
+ for my $train ( @{ $dep->{results} } ) {
+ if ( $train->line ne $traewelling->{line} ) {
+ next;
+ }
+ if ( not $train->sched_departure
+ or $train->sched_departure->epoch
+ != $traewelling->{dep_dt}->epoch )
+ {
+ next;
+ }
+ if (
+ not
+ List::Util::first { $_ eq $traewelling->{arr_name} }
+ $train->route_post
+ )
+ {
+ next;
+ }
+ $train_id = $train->train_id;
+ $train_ref = $train;
+ last;
+ }
+
+ if ( not $train_id ) {
+ $self->log->debug(
+ "... train $traewelling->{line} not found");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: Zug nicht gefunden",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ return $promise->resolve;
+ }
- return ( $distance_intermediate, $distance_beeline, $skipped );
+ $self->log->debug("... found train: $train_id");
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->checkin_p(
+ station => $traewelling->{dep_eva},
+ train_id => $train_id,
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ )->then(
+ sub {
+ $self->log->debug("... handled origin");
+ return $self->checkout_p(
+ station => $traewelling->{arr_eva},
+ train_id => 0,
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ );
+ }
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
+ if ( $traewelling->{message} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data =>
+ { comment => $traewelling->{message} }
+ );
+ }
+ $self->traewelling->log(
+ uid => $uid,
+ db => $db,
+ message =>
+"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
+ status_id => $traewelling->{status_id},
+ );
+ $self->traewelling->set_latest_pull_status_id(
+ uid => $uid,
+ status_id => $traewelling->{status_id},
+ db => $db
+ );
+
+ $tx->commit;
+ $promise->resolve;
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ $promise->resolve;
+ return;
+ }
+ )->wait;
+ }
+ )->catch(
+ sub {
+ my ( $err, $dep ) = @_;
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+ status_id => $traewelling->{status_id},
+ is_error => 1,
+ );
+ $promise->resolve;
+ return;
+ }
+ )->wait;
+
+ return $promise;
}
);
$self->helper(
- 'compute_journey_stats' => sub {
- my ( $self, @journeys ) = @_;
- my $km_route = 0;
- my $km_beeline = 0;
- my $min_travel_sched = 0;
- my $min_travel_real = 0;
- my $delay_dep = 0;
- my $delay_arr = 0;
- my $interchange_real = 0;
- my $num_trains = 0;
- my $num_journeys = 0;
- my @inconsistencies;
-
- my $next_departure = 0;
-
- for my $journey (@journeys) {
- $num_trains++;
- $km_route += $journey->{km_route};
- $km_beeline += $journey->{km_beeline};
- if ( $journey->{sched_duration}
- and $journey->{sched_duration} > 0 )
+ 'journeys_to_map_data' => sub {
+ my ( $self, %opt ) = @_;
+
+ my @journeys = @{ $opt{journeys} // [] };
+ my $route_type = $opt{route_type} // 'polybee';
+ my $include_manual = $opt{include_manual} ? 1 : 0;
+
+ my $location = $self->app->coordinates_by_station;
+
+ my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
+
+ if ( not @journeys ) {
+ return {
+ skipped_journeys => [],
+ station_coordinates => [],
+ polyline_groups => [],
+ };
+ }
+
+ my $json = JSON->new->utf8;
+
+ my $first_departure = $journeys[-1]->{rt_departure};
+ my $last_departure = $journeys[0]->{rt_departure};
+
+ my @stations = List::Util::uniq map { $_->{to_name} } @journeys;
+ push( @stations,
+ List::Util::uniq map { $_->{from_name} } @journeys );
+ @stations = List::Util::uniq @stations;
+ my @station_coordinates = map { [ $location->{$_}, $_ ] }
+ grep { exists $location->{$_} } @stations;
+
+ my @station_pairs;
+ my @polylines;
+ my %seen;
+
+ my @skipped_journeys;
+ my @polyline_journeys = grep { $_->{polyline} } @journeys;
+ my @beeline_journeys = grep { not $_->{polyline} } @journeys;
+
+ if ( $route_type eq 'polyline' ) {
+ @beeline_journeys = ();
+ }
+ elsif ( $route_type eq 'beeline' ) {
+ push( @beeline_journeys, @polyline_journeys );
+ @polyline_journeys = ();
+ }
+
+ for my $journey (@polyline_journeys) {
+ my @polyline = @{ $journey->{polyline} };
+ my $from_eva = $journey->{from_eva};
+ my $to_eva = $journey->{to_eva};
+
+ my $from_index
+ = first_index { $_->[2] and $_->[2] == $from_eva } @polyline;
+ my $to_index
+ = first_index { $_->[2] and $_->[2] == $to_eva } @polyline;
+
+ if ( $from_index == -1
+ or $to_index == -1 )
{
- $min_travel_sched += $journey->{sched_duration} / 60;
- }
- if ( $journey->{rt_duration} and $journey->{rt_duration} > 0 ) {
- $min_travel_real += $journey->{rt_duration} / 60;
+ # Fall back to route
+ delete $journey->{polyline};
+ next;
}
- if ( $journey->{sched_dep_ts} and $journey->{rt_dep_ts} ) {
- $delay_dep
- += ( $journey->{rt_dep_ts} - $journey->{sched_dep_ts} )
- / 60;
+
+ my $key
+ = $from_eva . '!'
+ . $to_eva . '!'
+ . ( $to_index - $from_index );
+
+ if ( $seen{$key} ) {
+ next;
}
- if ( $journey->{sched_arr_ts} and $journey->{rt_arr_ts} ) {
- $delay_arr
- += ( $journey->{rt_arr_ts} - $journey->{sched_arr_ts} )
- / 60;
+
+ $seen{$key} = 1;
+
+ # direction does not matter at the moment
+ $key
+ = $to_eva . '!'
+ . $from_eva . '!'
+ . ( $to_index - $from_index );
+ $seen{$key} = 1;
+
+ @polyline = @polyline[ $from_index .. $to_index ];
+ my @polyline_coords;
+ for my $coord (@polyline) {
+ push( @polyline_coords, [ $coord->[1], $coord->[0] ] );
}
+ push( @polylines, [@polyline_coords] );
+ }
- # Note that journeys are sorted from recent to older entries
- if ( $journey->{rt_arr_ts}
- and $next_departure
- and $next_departure - $journey->{rt_arr_ts} < ( 60 * 60 ) )
- {
- if ( $next_departure - $journey->{rt_arr_ts} < 0 ) {
- push( @inconsistencies,
- epoch_to_dt($next_departure)
- ->strftime('%d.%m.%Y %H:%M') );
+ for my $journey (@beeline_journeys) {
+
+ my @route = map { $_->[0] } @{ $journey->{route} };
+
+ my $from_index
+ = first_index { $_ eq $journey->{from_name} } @route;
+ my $to_index = first_index { $_ eq $journey->{to_name} } @route;
+
+ if ( $from_index == -1 ) {
+ my $rename = $self->app->renamed_station;
+ $from_index = first_index {
+ ( $rename->{$_} // $_ ) eq $journey->{from_name}
}
- else {
- $interchange_real
- += ( $next_departure - $journey->{rt_arr_ts} ) / 60;
+ @route;
+ }
+ if ( $to_index == -1 ) {
+ my $rename = $self->app->renamed_station;
+ $to_index = first_index {
+ ( $rename->{$_} // $_ ) eq $journey->{to_name}
}
+ @route;
+ }
+
+ if ( $from_index == -1
+ or $to_index == -1 )
+ {
+ push( @skipped_journeys,
+ [ $journey, 'Start/Ziel nicht in Route gefunden' ] );
+ next;
+ }
+
+ # Manual journey entries are only included if one of the following
+ # conditions is satisfied:
+ # * their route has more than two elements (-> probably more than just
+ # start and stop station), or
+ # * $include_manual is true (-> user wants to see incomplete routes)
+ # This avoids messing up the map in case an A -> B connection has been
+ # tracked both with a regular checkin (-> detailed route shown on map)
+ # and entered manually (-> beeline also shown on map, typically
+ # significantly differs from detailed route) -- unless the user
+ # sets include_manual, of course.
+ if ( $journey->{edited} & 0x0010
+ and @route <= 2
+ and not $include_manual )
+ {
+ push( @skipped_journeys,
+ [ $journey, 'Manueller Eintrag ohne Unterwegshalte' ] );
+ next;
+ }
+
+ @route = @route[ $from_index .. $to_index ];
+
+ my $key = join( '|', @route );
+
+ if ( $seen{$key} ) {
+ next;
}
- else {
- $num_journeys++;
+
+ $seen{$key} = 1;
+
+ # direction does not matter at the moment
+ $seen{ join( '|', reverse @route ) } = 1;
+
+ my $prev_station = shift @route;
+ for my $station (@route) {
+ push( @station_pairs, [ $prev_station, $station ] );
+ $prev_station = $station;
}
- $next_departure = $journey->{rt_dep_ts};
}
- return {
- km_route => $km_route,
- km_beeline => $km_beeline,
- num_trains => $num_trains,
- num_journeys => $num_journeys,
- min_travel_sched => $min_travel_sched,
- min_travel_real => $min_travel_real,
- min_interchange_real => $interchange_real,
- delay_dep => $delay_dep,
- delay_arr => $delay_arr,
- inconsistencies => \@inconsistencies,
+
+ @station_pairs = uniq_by { $_->[0] . '|' . $_->[1] } @station_pairs;
+ @station_pairs = grep {
+ exists $location->{ $_->[0] }
+ and exists $location->{ $_->[1] }
+ } @station_pairs;
+ @station_pairs
+ = map { [ $location->{ $_->[0] }, $location->{ $_->[1] } ] }
+ @station_pairs;
+
+ my $ret = {
+ skipped_journeys => \@skipped_journeys,
+ station_coordinates => \@station_coordinates,
+ polyline_groups => [
+ {
+ polylines => $json->encode( \@station_pairs ),
+ color => '#673ab7',
+ opacity => @polylines
+ ? $with_polyline
+ ? 0.4
+ : 0.6
+ : 0.8,
+ },
+ {
+ polylines => $json->encode( \@polylines ),
+ color => '#673ab7',
+ opacity => 0.8,
+ }
+ ],
};
+
+ if (@station_coordinates) {
+ my @lats = map { $_->[0][0] } @station_coordinates;
+ my @lons = map { $_->[0][1] } @station_coordinates;
+ my $min_lat = List::Util::min @lats;
+ my $max_lat = List::Util::max @lats;
+ my $min_lon = List::Util::min @lons;
+ my $max_lon = List::Util::max @lons;
+ $ret->{bounds}
+ = [ [ $min_lat, $min_lon ], [ $max_lat, $max_lon ] ];
+ }
+
+ return $ret;
}
);
@@ -3274,71 +2351,108 @@ sub startup {
$r->get('/changelog')->to('static#changelog');
$r->get('/impressum')->to('static#imprint');
$r->get('/imprint')->to('static#imprint');
- $r->get('/offline')->to('static#offline');
+ $r->get('/legend')->to('static#legend');
+ $r->get('/offline.html')->to('static#offline');
$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
$r->get('/login')->to('account#login_form');
$r->get('/recover')->to('account#request_password_reset');
$r->get('/recover/:id/:token')->to('account#recover_password');
- $r->get('/register')->to('account#registration_form');
$r->get('/reg/:id/:token')->to('account#verify');
- $r->get('/status/:name')->to('traveling#user_status');
- $r->get('/status/:name/:ts')->to('traveling#user_status');
- $r->get('/ajax/status/:name')->to('traveling#public_status_card');
- $r->get('/ajax/status/:name/:ts')->to('traveling#public_status_card');
+ $r->get( '/status/:name' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
+ $r->get( '/status/:name/:ts' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
+ $r->get('/ajax/status/#name')->to('profile#status_card');
+ $r->get('/ajax/status/:name/:ts')->to('profile#status_card');
+ $r->get('/p/:name')->to('profile#profile');
+ $r->get( '/p/:name/j/:id' => 'public_journey' )
+ ->to('profile#journey_details');
+ $r->get('/.well-known/webfinger')->to('account#webfinger');
+ $r->get('/dyn/:av/autocomplete.js')->to('api#autocomplete');
$r->post('/api/v1/import')->to('api#import_v1');
$r->post('/api/v1/travel')->to('api#travel_v1');
- $r->post('/action')->to('traveling#log_action');
+ $r->post('/action')->to('traveling#travel_action');
$r->post('/geolocation')->to('traveling#geolocation');
$r->post('/list_departures')->to('traveling#redirect_to_station');
$r->post('/login')->to('account#do_login');
- $r->post('/register')->to('account#register');
$r->post('/recover')->to('account#request_password_reset');
+ if ( $self->config->{traewelling}{oauth} ) {
+ $r->get('/oauth/traewelling')->to('traewelling#oauth');
+ $r->post('/oauth/traewelling')->to('traewelling#oauth');
+ }
+
+ if ( not $self->config->{registration}{disabled} ) {
+ $r->get('/register')->to('account#registration_form');
+ $r->post('/register')->to('account#register');
+ }
+
my $authed_r = $r->under(
sub {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
return 1;
}
- $self->render( 'login', redirect_to => $self->req->url );
+ $self->render(
+ 'login',
+ redirect_to => $self->req->url,
+ from => 'auth_required'
+ );
return undef;
}
);
$authed_r->get('/account')->to('account#account');
$authed_r->get('/account/privacy')->to('account#privacy');
+ $authed_r->get('/account/social')->to('account#social');
+ $authed_r->get('/account/social/:kind')->to('account#social_list');
+ $authed_r->get('/account/profile')->to('account#profile');
$authed_r->get('/account/hooks')->to('account#webhook');
+ $authed_r->get('/account/traewelling')->to('traewelling#settings');
$authed_r->get('/account/insight')->to('account#insight');
+ $authed_r->get('/account/services')->to('account#services');
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
$authed_r->get('/cancelled')->to('traveling#cancelled');
$authed_r->get('/fgr')->to('passengerrights#list_candidates');
$authed_r->get('/account/password')->to('account#password_form');
$authed_r->get('/account/mail')->to('account#change_mail');
+ $authed_r->get('/account/name')->to('account#change_name');
$authed_r->get('/export.json')->to('account#json_export');
$authed_r->get('/history.json')->to('traveling#json_history');
+ $authed_r->get('/history.csv')->to('traveling#csv_history');
$authed_r->get('/history')->to('traveling#history');
+ $authed_r->get('/history/commute')->to('traveling#commute');
$authed_r->get('/history/map')->to('traveling#map_history');
$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/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('/s/*station')->to('traveling#station');
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
$authed_r->post('/account/privacy')->to('account#privacy');
+ $authed_r->post('/account/social')->to('account#social');
+ $authed_r->post('/account/profile')->to('account#profile');
$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('/history/map')->to('traveling#map_history');
+ $authed_r->post('/account/services')->to('account#services');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
$authed_r->post('/journey/comment')->to('traveling#comment_form');
+ $authed_r->post('/journey/visibility')->to('traveling#visibility_form');
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
$authed_r->post('/journey/passenger_rights/*filename')
->to('passengerrights#generate');
$authed_r->post('/account/password')->to('account#change_password');
$authed_r->post('/account/mail')->to('account#change_mail');
+ $authed_r->post('/account/name')->to('account#change_name');
+ $authed_r->post('/social-action')->to('account#social_action');
$authed_r->post('/delete')->to('account#delete');
$authed_r->post('/logout')->to('account#do_logout');
$authed_r->post('/set_token')->to('api#set_token');
+ $authed_r->get('/timeline/in-transit')->to('profile#checked_in');
}
diff --git a/lib/Travelynx/Command/account.pm b/lib/Travelynx/Command/account.pm
new file mode 100644
index 0000000..1d17400
--- /dev/null
+++ b/lib/Travelynx/Command/account.pm
@@ -0,0 +1,119 @@
+package Travelynx::Command::account;
+
+# Copyright (C) 2021 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Command';
+use UUID::Tiny qw(:std);
+
+has description => 'Add or remove user accounts';
+
+has usage => sub { shift->extract_usage };
+
+sub add_user {
+ my ( $self, $name, $email ) = @_;
+
+ my $db = $self->app->pg->db;
+
+ if ( my $error = $self->app->users->is_name_invalid( name => $name ) ) {
+ say "Cannot add account '$name': $error";
+ die;
+ }
+
+ my $token = "tmp";
+ my $password = substr( create_uuid_as_string(UUID_V4), 0, 18 );
+
+ my $tx = $db->begin;
+ my $user_id = $self->app->users->add(
+ db => $db,
+ name => $name,
+ email => $email,
+ token => $token,
+ password => $password,
+ );
+ my $success = $self->app->users->verify_registration_token(
+ db => $db,
+ uid => $user_id,
+ token => $token,
+ in_transaction => 1,
+ );
+
+ if ($success) {
+ $tx->commit;
+ say "Added user $name ($email) with UID $user_id";
+ say "Temporary password for login: $password";
+ }
+}
+
+sub delete_user {
+ my ( $self, $uid ) = @_;
+
+ my $user_data = $self->app->users->get( uid => $uid );
+
+ if ( not $user_data ) {
+ say "UID $uid does not exist.";
+ return;
+ }
+
+ $self->app->users->flag_deletion( uid => $uid );
+
+ say "User $user_data->{name} (UID $uid) has been flagged for deletion.";
+}
+
+sub really_delete_user {
+ my ( $self, $uid, $name ) = @_;
+
+ my $user_data = $self->app->users->get( uid => $uid );
+
+ if ( $user_data->{name} ne $name ) {
+ say
+ "User name $name does not match UID $uid. Account deletion aborted.";
+ return;
+ }
+
+ my $count = $self->app->users->delete( uid => $uid );
+
+ printf( "Deleted %s -- %d tokens, %d monthly stats, %d journeys\n",
+ $name, $count->{tokens}, $count->{stats}, $count->{journeys} );
+
+ return;
+}
+
+sub run {
+ my ( $self, $command, @args ) = @_;
+
+ if ( $command eq 'add' ) {
+ $self->add_user(@args);
+ }
+ elsif ( $command eq 'delete' ) {
+ $self->delete_user(@args);
+ }
+ elsif ( $command eq 'DELETE' ) {
+ $self->really_delete_user(@args);
+ }
+ else {
+ $self->help;
+ }
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl account add [name] [email]
+
+ Adds user [name] with a temporary password, which is shown on stdout.
+ Users can change the password once logged in.
+
+ Usage: index.pl account delete [uid]
+
+ Request deletion of user [uid]. This has the same effect as using the
+ account deletion button. The user account and all corresponding data will
+ be deleted by a maintenance run after three days.
+
+ Usage: index.pl account DELETE [uid] [name]
+
+ Immediately delete user [uid]/[name] and all associated data. Deletion is
+ irrevocable. Deletion is only performed if [name] matches the name of [uid].
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index cb5ffec..d13b2a7 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1,7 +1,13 @@
package Travelynx::Command::database;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use DateTime;
+use File::Slurp qw(read_file);
+use JSON;
use Travel::Status::DE::IRIS::Stations;
has description => 'Initialize or upgrade database layout';
@@ -9,13 +15,12 @@ has description => 'Initialize or upgrade database layout';
has usage => sub { shift->extract_usage };
sub get_schema_version {
- my ($db) = @_;
+ my ( $db, $key ) = @_;
my $version;
- eval {
- $version
- = $db->select( 'schema_version', ['version'] )->hash->{version};
- };
+ $key //= 'version';
+
+ eval { $version = $db->select( 'schema_version', [$key] )->hash->{$key}; };
if ($@) {
# If it failed, the version table does not exist -> run setup first.
@@ -941,8 +946,1196 @@ my @migrations = (
}
);
},
+
+ # v19 -> v20
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table polylines (
+ id serial not null primary key,
+ origin_eva integer not null,
+ destination_eva integer not null,
+ polyline jsonb not null
+ );
+ alter table journeys
+ add column polyline_id integer references polylines (id);
+ alter table in_transit
+ add column polyline_id integer references polylines (id);
+ drop view journeys_str;
+ drop view in_transit_str;
+ create view journeys_str as select
+ journeys.id as journey_id, user_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,
+ 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,
+ polylines.polyline as polyline,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ ;
+ create or replace view in_transit_str as select
+ user_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,
+ 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,
+ polylines.polyline as polyline,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ ;
+ update schema_version set version = 20;
+ }
+ );
+ },
+
+ # v20 -> v21
+ # After introducing polyline support, journey distance calculation diverged:
+ # the detail view (individual train) used the polyline, whereas monthly and
+ # yearly statistics were still based on beeline between intermediate stops.
+ # Release 1.16.0 fixes this -> ensure all caches are rebuilt.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ truncate journey_stats;
+ update schema_version set version = 21;
+ }
+ );
+ },
+
+ # v21 -> v22
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table traewelling (
+ user_id integer not null references users (id) primary key,
+ email varchar(256) not null,
+ push_sync boolean not null,
+ pull_sync boolean not null,
+ errored boolean,
+ token text,
+ data jsonb,
+ latest_run timestamptz
+ );
+ comment on table traewelling is 'Token and Status for Traewelling';
+ create view traewelling_str as select
+ user_id, email, push_sync, pull_sync, errored, token, data,
+ extract(epoch from latest_run) as latest_run_ts
+ from traewelling
+ ;
+ update schema_version set version = 22;
+ }
+ );
+ },
+
+ # v22 -> v23
+ # 1.18.1 fixes handling of negative cumulative arrival/departure delays
+ # and introduces additional statistics entries with pre-formatted duration
+ # strings while at it. Old cache entries lack those.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ truncate journey_stats;
+ update schema_version set version = 23;
+ }
+ );
+ },
+
+ # v23 -> v24
+ # travelynx 1.22 warns about upcoming account deletion due to inactivity
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users add column deletion_notified timestamptz;
+ comment on column users.deletion_notified is 'Time at which warning about upcoming account deletion due to inactivity was sent';
+ update schema_version set version = 24;
+ }
+ );
+ },
+
+ # v24 -> v25
+ # travelynx 1.23 adds optional links to external services, e.g.
+ # DBF or bahn.expert departure boards
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users add column external_services smallint;
+ comment on column users.external_services is 'Which external service to use for stationboard or routing links';
+ update schema_version set version = 25;
+ }
+ );
+ },
+
+ # v25 -> v26
+ # travelynx 1.24 adds local transit connections and needs to know targets
+ # for that to work, as local transit does not support checkins yet.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table localtransit (
+ user_id integer not null references users (id) primary key,
+ data jsonb
+ );
+ create view user_transit as select
+ id,
+ use_history,
+ localtransit.data as data
+ from users
+ left join localtransit on localtransit.user_id = id
+ ;
+ update schema_version set version = 26;
+ }
+ );
+ },
+
+ # v26 -> v27
+ # add list of stations that are not (or no longer) present in T-S-DE-IRIS
+ # (in this case, stations that were removed up to 1.74)
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version
+ add column iris varchar(12);
+ create table stations (
+ eva int not null primary key,
+ ds100 varchar(16) not null,
+ name varchar(64) not null,
+ lat real not null,
+ lon real not null,
+ source smallint not null,
+ archived bool not null
+ );
+ update schema_version set version = 27;
+ update schema_version set iris = '0';
+ }
+ );
+ },
+
+ # v27 -> v28
+ # add ds100, name, and lat/lon from stations table to journeys_str / in_transit_str
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view journeys_str;
+ drop view in_transit_str;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ create view in_transit_str as select
+ user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ update schema_version set version = 28;
+ }
+ );
+ },
+
+ # v28 -> v29
+ # add pre-migration travelynx version. This way, a failed migration can
+ # print a helpful "git checkout" command.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version
+ add column travelynx varchar(64);
+ update schema_version set version = 29;
+ }
+ );
+ },
+
+ # v29 -> v30
+ # change layout of stops in in_transit and journeys "route" lists.
+ # Old layout: A mixture of [name, {data}, undef/"additional"/"cancelled"], [name, timestamp, timestamp], and [name]
+ # New layout: [name, eva, {data including isAdditional/isCancelled}]
+ # Combined with a maintenance task that adds eva IDs to past stops, this will allow for more resilience against station name changes.
+ # It will also help increase the performance of distance and map calculation
+ sub {
+ my ($db) = @_;
+ my $json = JSON->new;
+
+ say 'Adjusting route schema, this may take a while ...';
+
+ my $res = $db->select( 'in_transit_str', [ 'route', 'user_id' ] );
+ while ( my $row = $res->expand->hash ) {
+ my @new_route;
+ for my $stop ( @{ $row->{route} } ) {
+ push( @new_route, [ $stop->[0], undef, {} ] );
+ }
+ $db->update(
+ 'in_transit',
+ { route => $json->encode( \@new_route ) },
+ { user_id => $row->{user_id} }
+ );
+ }
+
+ my $total
+ = $db->select( 'journeys', 'count(*) as count' )->hash->{count};
+ my $count = 0;
+
+ $res = $db->select( 'journeys_str', [ 'route', 'journey_id' ] );
+ while ( my $row = $res->expand->hash ) {
+ my @new_route;
+
+ for my $stop ( @{ $row->{route} } ) {
+ if ( @{$stop} == 1 ) {
+ push( @new_route, [ $stop->[0], undef, {} ] );
+ }
+ elsif (
+ ( not defined $stop->[1] or $stop->[1] =~ m{ ^ \d+ $ }x )
+ and
+ ( not defined $stop->[2] or $stop->[2] =~ m{ ^ \d+ $ }x )
+ )
+ {
+ push( @new_route, [ $stop->[0], undef, {} ] );
+ }
+ else {
+ my $attr = $stop->[1] // {};
+ if ( $stop->[2] and $stop->[2] eq 'additional' ) {
+ $attr->{isAdditional} = 1;
+ }
+ elsif ( $stop->[2] and $stop->[2] eq 'cancelled' ) {
+ $attr->{isCancelled} = 1;
+ }
+ push( @new_route, [ $stop->[0], undef, $attr ] );
+ }
+ }
+
+ $db->update(
+ 'journeys',
+ { route => $json->encode( \@new_route ) },
+ { id => $row->{journey_id} }
+ );
+
+ if ( $count++ % 10000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+ say ' done';
+ $db->query(
+ qq{
+ update schema_version set version = 30;
+ }
+ );
+ },
+
+ # v30 -> v31
+ # travelynx v1.29.17 introduces links to conflicting journeys.
+ # These require changes to statistics data.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ truncate journey_stats;
+ update schema_version set version = 31;
+ }
+ );
+ },
+
+ # v31 -> v32
+ # travelynx v1.29.18 improves above-mentioned conflict links.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ truncate journey_stats;
+ update schema_version set version = 32;
+ }
+ );
+ },
+
+ # v32 -> v33
+ # add optional per-status visibility that overrides global visibility
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table journeys add column visibility smallint;
+ alter table in_transit add column visibility smallint;
+ drop view journeys_str;
+ drop view in_transit_str;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ create view in_transit_str as select
+ user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ }
+ );
+ my $res = $db->select( 'users', [ 'id', 'public_level' ] );
+ while ( my $row = $res->hash ) {
+ my $old_level = $row->{public_level};
+
+ # status default: unlisted
+ my $new_level = 30;
+ if ( $old_level & 0x01 ) {
+
+ # status: account required
+ $new_level = 80;
+ }
+ if ( $old_level & 0x02 ) {
+
+ # status: public
+ $new_level = 100;
+ }
+ if ( $old_level & 0x04 ) {
+
+ # comment public
+ $new_level |= 0x80;
+ }
+ if ( $old_level & 0x10 ) {
+
+ # past: account required
+ $new_level |= 0x100;
+ }
+ if ( $old_level & 0x20 ) {
+
+ # past: public
+ $new_level |= 0x200;
+ }
+ if ( $old_level & 0x40 ) {
+
+ # past: infinite (default is 4 weeks)
+ $new_level |= 0x400;
+ }
+ my $r = $db->update(
+ 'users',
+ { public_level => $new_level },
+ { id => $row->{id} }
+ )->rows;
+ if ( $r != 1 ) {
+ die("oh no");
+ }
+ }
+ $db->update( 'schema_version', { version => 33 } );
+ },
+
+ # v33 -> v34
+ # add polyline_id to in_transit_str
+ # (https://github.com/derf/travelynx/issues/66)
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ create view in_transit_str as select
+ user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ update schema_version set version = 34;
+ }
+ );
+ },
+
+ # v34 -> v35
+ sub {
+ my ($db) = @_;
+
+ # 1 : follows
+ # 2 : follow requested
+ # 3 : is blocked by
+ $db->query(
+ qq{
+ create table relations (
+ subject_id integer not null references users (id),
+ predicate smallint not null,
+ object_id integer not null references users (id),
+ primary key (subject_id, object_id)
+ );
+ create view followers as select
+ relations.object_id as self_id,
+ users.id as id,
+ users.name as name
+ from relations
+ join users on relations.subject_id = users.id
+ where predicate = 1;
+ create view followees as select
+ relations.subject_id as self_id,
+ users.id as id,
+ users.name as name
+ from relations
+ join users on relations.object_id = users.id
+ where predicate = 1;
+ create view follow_requests as select
+ relations.object_id as self_id,
+ users.id as id,
+ users.name as name
+ from relations
+ join users on relations.subject_id = users.id
+ where predicate = 2;
+ create view blocked_users as select
+ relations.object_id as self_id,
+ users.id as id,
+ users.name as name
+ from relations
+ join users on relations.subject_id = users.id
+ where predicate = 3;
+ update schema_version set version = 35;
+ }
+ );
+ },
+
+ # v35 -> v36
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table relations
+ add column ts timestamptz not null;
+ alter table users
+ add column accept_follows smallint default 0;
+ update schema_version set version = 36;
+ }
+ );
+ },
+
+ # v36 -> v37
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users
+ add column notifications smallint default 0,
+ add column profile jsonb;
+ update schema_version set version = 37;
+ }
+ );
+ },
+
+ # v37 -> v38
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view followers;
+ create view followers as select
+ relations.object_id as self_id,
+ users.id as id,
+ users.name as name,
+ users.accept_follows as accept_follows,
+ r2.predicate as inverse_predicate
+ from relations
+ join users on relations.subject_id = users.id
+ left join relations as r2 on relations.subject_id = r2.object_id
+ where relations.predicate = 1;
+ update schema_version set version = 38;
+ }
+ );
+ },
+
+ # v38 -> v39
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view followers;
+ create view followers as select
+ relations.object_id as self_id,
+ users.id as id,
+ users.name as name,
+ users.accept_follows as accept_follows,
+ r2.predicate as inverse_predicate
+ from relations
+ join users on relations.subject_id = users.id
+ left join relations as r2
+ on relations.subject_id = r2.object_id
+ and relations.object_id = r2.subject_id
+ where relations.predicate = 1;
+ update schema_version set version = 39;
+ }
+ );
+ },
+
+ # v39 -> v40
+ # distinguish between public / travelynx / followers / private visibility
+ # for the history page, just like status visibility.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users alter public_level type integer;
+ }
+ );
+ my $res = $db->select( 'users', [ 'id', 'public_level' ] );
+ while ( my $row = $res->hash ) {
+ my $old_level = $row->{public_level};
+
+ # checkin and comment visibility remain unchanged
+ my $new_level = $old_level & 0x00ff;
+
+ # past: account required
+ if ( $old_level & 0x100 ) {
+ $new_level |= 80 << 8;
+ }
+
+ # past: public
+ elsif ( $old_level & 0x200 ) {
+ $new_level |= 100 << 8;
+ }
+
+ # past: private
+ else {
+ $new_level |= 10 << 8;
+ }
+
+ # past: infinite (default is 4 weeks)
+ if ( $old_level & 0x400 ) {
+ $new_level |= 0x10000;
+ }
+
+ # show past journey on status page
+ if ( $old_level & 0x800 ) {
+ $new_level |= 0x8000;
+ }
+
+ my $r = $db->update(
+ 'users',
+ { public_level => $new_level },
+ { id => $row->{id} }
+ )->rows;
+ if ( $r != 1 ) {
+ die("oh no");
+ }
+ }
+ $db->update( 'schema_version', { version => 40 } );
+ },
+
+ # v40 -> v41
+ # Compute effective visibility in in_transit_str and journeys_str.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ drop view journeys_str;
+ create view in_transit_str as select
+ user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ update schema_version set version = 41;
+ }
+ );
+ },
+
+ # v41 -> v42
+ # adds current followee checkins
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ ;
+ update schema_version set version = 42;
+ }
+ );
+ },
+
+ # v42 -> v43
+ # list sent and received follow requests
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter view follow_requests rename to rx_follow_requests;
+ create view tx_follow_requests as select
+ relations.subject_id as self_id,
+ users.id as id,
+ users.name as name
+ from relations
+ join users on relations.object_id = users.id
+ where predicate = 2;
+ update schema_version set version = 43;
+ }
+ );
+ },
+
+ # v43 -> v44
+ # show inverse relation in followees as well
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view followees;
+ create view followees as select
+ relations.subject_id as self_id,
+ users.id as id,
+ users.name as name,
+ r2.predicate as inverse_predicate
+ from relations
+ join users on relations.object_id = users.id
+ left join relations as r2
+ on relations.subject_id = r2.object_id
+ and relations.object_id = r2.subject_id
+ where relations.predicate = 1;
+ update schema_version set version = 44;
+ }
+ );
+ },
+
+ # v44 -> v45
+ # prepare for HAFAS support: many HAFAS stations do not have DS100 codes
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table stations alter column ds100 drop not null;
+ update schema_version set version = 45;
+ }
+ );
+ },
+
+ # v45 -> v46
+ # Switch to Traewelling OAuth2 authentication.
+ # E-Mail is no longer needed.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view traewelling_str;
+ create view traewelling_str as select
+ user_id, push_sync, pull_sync, errored, token, data,
+ extract(epoch from latest_run) as latest_run_ts
+ from traewelling
+ ;
+ alter table traewelling drop column email;
+ update schema_version set version = 46;
+ }
+ );
+ },
+
+ # v46 -> v47
+ # sort followee checkins by checkin time
+ # (descending / most recent first, like a timeline)
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view follows_in_transit;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ order by checkin_time desc
+ ;
+ update schema_version set version = 47;
+ }
+ );
+ },
+
+ # v47 -> v48
+ # Store Traewelling refresh tokens; store expiry as explicit column.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table traewelling
+ add column refresh_token text,
+ add column expiry timestamptz;
+ drop view traewelling_str;
+ create view traewelling_str as select
+ user_id, push_sync, pull_sync, errored,
+ token, refresh_token, data,
+ extract(epoch from latest_run) as latest_run_ts,
+ extract(epoch from expiry) as expiry_ts
+ from traewelling
+ ;
+ update schema_version set version = 48;
+ }
+ );
+ },
+
+ # v48 -> v49
+ # create indexes for common queries
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create index uid_real_departure_idx on journeys (user_id, real_departure);
+ update schema_version set version = 49;
+ }
+ );
+ },
+
+ # v49 -> v50
+ # travelynx 2.0 introduced proper HAFAS support, so there is no need for
+ # the 'FYI, here is some hAFAS data' kludge anymore.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view user_transit;
+ drop table localtransit;
+ update schema_version set version = 50;
+ }
+ );
+ },
+
+ # v50 -> v51
+ # store related HAFAS stations
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table related_stations (
+ eva integer not null,
+ meta integer not null,
+ unique (eva, meta)
+ );
+ create index rel_eva on related_stations (eva);
+ update schema_version set version = 51;
+ }
+ );
+ },
);
+sub sync_stations {
+ my ( $db, $iris_version ) = @_;
+
+ $db->update( 'schema_version',
+ { iris => $Travel::Status::DE::IRIS::Stations::VERSION } );
+
+ say 'Updating stations table, this may take a while ...';
+ my $total = scalar Travel::Status::DE::IRIS::Stations::get_stations();
+ my $count = 0;
+ for my $s ( Travel::Status::DE::IRIS::Stations::get_stations() ) {
+ my ( $ds100, $name, $eva, $lon, $lat ) = @{$s};
+ if ( $ENV{__TRAVELYNX_TEST_MINI_IRIS}
+ and ( $eva < 8000000 or $eva > 8000100 ) )
+ {
+ next;
+ }
+ $db->insert(
+ 'stations',
+ {
+ eva => $eva,
+ ds100 => $ds100,
+ name => $name,
+ lat => $lat,
+ lon => $lon,
+ source => 0,
+ archived => 0
+ },
+ {
+ on_conflict => \
+'(eva) do update set archived = false, source = 0, ds100 = EXCLUDED.ds100, name=EXCLUDED.name, lat=EXCLUDED.lat, lon=EXCLUDED.lon'
+ }
+ );
+ if ( $count++ % 1000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+ say ' done';
+
+ my $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null
+ limit 1;
+ }
+ )->hash;
+
+ my $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null
+ limit 1;
+ }
+ )->hash;
+
+ if ( $res1 or $res2 ) {
+ say 'Dropping stats cache for archived stations ...';
+ $db->query('truncate journey_stats;');
+ }
+
+ say 'Updating archived stations ...';
+ my $old_stations
+ = JSON->new->utf8->decode( scalar read_file('share/old_stations.json') );
+ if ( $ENV{__TRAVELYNX_TEST_MINI_IRIS} ) {
+ $old_stations = [];
+ }
+ for my $s ( @{$old_stations} ) {
+ $db->insert(
+ 'stations',
+ {
+ eva => $s->{eva},
+ ds100 => $s->{ds100},
+ name => $s->{name},
+ lat => $s->{latlong}[0],
+ lon => $s->{latlong}[1],
+ source => 0,
+ archived => 1
+ },
+ { on_conflict => undef }
+ );
+ }
+
+ if ( $iris_version == 0 ) {
+ say 'Applying EVA ID changes ...';
+ for my $change (
+ [ 721394, 301002, 'RKBP: Kronenplatz (U), Karlsruhe' ],
+ [
+ 721356, 901012,
+ 'RKME: Ettlinger Tor/Staatstheater (U), Karlsruhe'
+ ],
+ )
+ {
+ my ( $old, $new, $desc ) = @{$change};
+ my $rows = $db->update(
+ 'journeys',
+ { checkout_station_id => $new },
+ { checkout_station_id => $old }
+ )->rows;
+ $rows += $db->update(
+ 'journeys',
+ { checkin_station_id => $new },
+ { checkin_station_id => $old }
+ )->rows;
+ if ($rows) {
+ say "$desc ($old -> $new) : $rows rows";
+ }
+ }
+ }
+
+ say 'Checking for unknown EVA IDs ...';
+ my $found = 0;
+
+ $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ my %notified;
+ while ( my $row = $res1->hash ) {
+ my $eva = $row->{checkin_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say '';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ while ( my $row = $res2->hash ) {
+ my $eva = $row->{checkout_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say '';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ if ($found) {
+ say '------------8<----------';
+ say '';
+ say
+'Due to a conceptual flaw in past travelynx releases, your database contains unknown EVA IDs.';
+ say
+'Please file a bug report titled "Missing EVA IDs after DB migration" at https://github.com/derf/travelynx/issues';
+ say 'and include the list shown above in the bug report.';
+ say
+'If you do not have a GitHub account, please send an E-Mail to derf+travelynx@finalrewind.org instead.';
+ say '';
+ say 'This issue does not affect usability or long-term data integrity,';
+ say 'and handling it is not time-critical.';
+ say
+'Past journeys referencing unknown EVA IDs may have inaccurate distance statistics,';
+ say
+'but this will be resolved once a future release handles those EVA IDs.';
+ say 'Note that this issue was already present in previous releases.';
+ }
+ else {
+ say 'None found.';
+ }
+}
+
sub setup_db {
my ($db) = @_;
my $tx = $db->begin;
@@ -956,31 +2149,86 @@ sub setup_db {
}
}
+sub failure_hints {
+ my ($old_version) = @_;
+ say STDERR 'This travelynx instance has reached an undefined state:';
+ say STDERR
+'The source code is expecting a different schema version than present in the database.';
+ say STDERR
+'Please file a detailed bug report at <https://github.com/derf/travelynx/issues>';
+ say STDERR 'or send an e-mail to derf+travelynx@finalrewind.org.';
+ if ($old_version) {
+ say STDERR '';
+ say STDERR
+ "The last migration was performed with travelynx v${old_version}.";
+ say STDERR
+'You may be able to return to a working state with the following command:';
+ say STDERR "git checkout ${old_version}";
+ say STDERR '';
+ say STDERR 'We apologize for any inconvenience.';
+ }
+}
+
sub migrate_db {
- my ($db) = @_;
+ my ( $self, $db ) = @_;
my $tx = $db->begin;
my $schema_version = get_schema_version($db);
say "Found travelynx schema v${schema_version}";
+ my $old_version;
+
+ if ( $schema_version >= 29 ) {
+ $old_version = get_schema_version( $db, 'travelynx' );
+ }
+
if ( $schema_version == @migrations ) {
- say "Database layout is up-to-date";
+ say 'Database layout is up-to-date';
+ }
+ else {
+ eval {
+ for my $i ( $schema_version .. $#migrations ) {
+ printf( "Updating to v%d ...\n", $i + 1 );
+ $migrations[$i]($db);
+ }
+ say 'Update complete.';
+ };
+ if ($@) {
+ say STDERR "Migration failed: $@";
+ say STDERR "Rolling back to v${schema_version}";
+ failure_hints($old_version);
+ exit(1);
+ }
}
- eval {
- for my $i ( $schema_version .. $#migrations ) {
- printf( "Updating to v%d ...\n", $i + 1 );
- $migrations[$i]($db);
+ my $iris_version = get_schema_version( $db, 'iris' );
+ say "Found IRIS station database v${iris_version}";
+ if ( $iris_version eq $Travel::Status::DE::IRIS::Stations::VERSION ) {
+ say 'Station database is up-to-date';
+ }
+ else {
+ eval {
+ say
+"Synchronizing with Travel::Status::DE::IRIS $Travel::Status::DE::IRIS::Stations::VERSION";
+ sync_stations( $db, $iris_version );
+ say 'Synchronization complete.';
+ };
+ if ($@) {
+ say STDERR "Synchronization failed: $@";
+ if ( $schema_version != @migrations ) {
+ say STDERR "Rolling back to v${schema_version}";
+ failure_hints($old_version);
+ }
+ exit(1);
}
- };
- if ($@) {
- say STDERR "Migration failed: $@";
- say STDERR "Rolling back to v${schema_version}";
- exit(1);
}
+ $db->update( 'schema_version',
+ { travelynx => $self->app->config->{version} } );
+
if ( get_schema_version($db) == @migrations ) {
$tx->commit;
+ say 'Changes committed to database. Have a nice day.';
}
else {
printf STDERR (
@@ -989,6 +2237,8 @@ sub migrate_db {
get_schema_version($db)
);
say STDERR "Rolling back to v${schema_version}";
+ say STDERR "";
+ failure_hints($old_version);
exit(1);
}
}
@@ -1007,10 +2257,13 @@ sub run {
if ( not defined get_schema_version($db) ) {
setup_db($db);
}
- migrate_db($db);
+ $self->migrate_db($db);
}
elsif ( $command eq 'has-current-schema' ) {
- if ( get_schema_version($db) == @migrations ) {
+ if ( get_schema_version($db) == @migrations
+ and get_schema_version( $db, 'iris' ) eq
+ $Travel::Status::DE::IRIS::Stations::VERSION )
+ {
say "yes";
}
else {
@@ -1035,5 +2288,5 @@ __END__
Recommended workflow:
> systemctl stop travelynx
- > perl index.pl migrate
+ > perl index.pl database migrate
> systemctl start travelynx
diff --git a/lib/Travelynx/Command/dumpconfig.pm b/lib/Travelynx/Command/dumpconfig.pm
index d2a6761..600ffb0 100644
--- a/lib/Travelynx/Command/dumpconfig.pm
+++ b/lib/Travelynx/Command/dumpconfig.pm
@@ -1,4 +1,7 @@
package Travelynx::Command::dumpconfig;
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use Data::Dumper;
diff --git a/lib/Travelynx/Command/dumpstops.pm b/lib/Travelynx/Command/dumpstops.pm
new file mode 100644
index 0000000..e6740ec
--- /dev/null
+++ b/lib/Travelynx/Command/dumpstops.pm
@@ -0,0 +1,51 @@
+package Travelynx::Command::dumpstops;
+
+# Copyright (C) 2024 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use Mojo::Base 'Mojolicious::Command';
+use List::Util qw();
+use Text::CSV;
+
+has description => 'Export HAFAS/IRIS stops to CSV';
+
+has usage => sub { shift->extract_usage };
+
+sub run {
+ my ( $self, $command, $filename ) = @_;
+ my $db = $self->app->pg->db;
+
+ if ( not $command or not $filename ) {
+ $self->help;
+ }
+ elsif ( $command eq 'csv' ) {
+ open( my $fh, '>:encoding(utf-8)', $filename )
+ or die("open($filename): $!\n");
+
+ my $csv = Text::CSV->new( { eol => "\r\n" } );
+ $csv->combine(qw(name eva lat lon source archived));
+ print $fh $csv->string;
+
+ my $iter = $self->app->stations->get_db_iterator;
+ while ( my $row = $iter->hash ) {
+ $csv->combine( @{$row}{qw{name eva lat lon source archived}} );
+ print $fh $csv->string;
+ }
+ close($fh);
+ }
+ else {
+ $self->help;
+ }
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl dumpstops <format> <filename>
+
+ Exports known stops to <filename>.
+ Right now, only the "csv" format is supported.
diff --git a/lib/Travelynx/Command/influxdb.pm b/lib/Travelynx/Command/influxdb.pm
new file mode 100644
index 0000000..f3fc3de
--- /dev/null
+++ b/lib/Travelynx/Command/influxdb.pm
@@ -0,0 +1,209 @@
+package Travelynx::Command::influxdb;
+
+# Copyright (C) 2022 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Command';
+
+use DateTime;
+
+has description => 'Generate statistics for InfluxDB';
+
+has usage => sub { shift->extract_usage };
+
+sub query_to_influx {
+ my ( $label, $value ) = @_;
+
+ if ( defined $value ) {
+ return sprintf( '%s=%f', $label, $value );
+ }
+ return;
+}
+
+sub run {
+ my ($self) = @_;
+
+ my $db = $self->app->pg->db;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $active = $now->clone->subtract( months => 1 );
+
+ my @stats;
+ my @stations;
+ my @traewelling;
+
+ push(
+ @stats,
+ query_to_influx(
+ 'pending_user_count',
+ $db->select( 'users', 'count(*) as count', { status => 0 } )
+ ->hash->{count}
+ )
+ );
+ push(
+ @stats,
+ query_to_influx(
+ 'reg_user_count',
+ $db->select( 'users', 'count(*) as count', { status => 1 } )
+ ->hash->{count}
+ )
+ );
+ push(
+ @stats,
+ query_to_influx(
+ 'active_user_count',
+ $db->select(
+ 'users',
+ 'count(*) as count',
+ {
+ status => 1,
+ last_seen => { '>', $active }
+ }
+ )->hash->{count}
+ )
+ );
+
+ push(
+ @stats,
+ query_to_influx(
+ 'checked_in_count',
+ $db->select( 'in_transit', 'count(*) as count' )->hash->{count}
+ )
+ );
+ push(
+ @stats,
+ query_to_influx(
+ 'checkin_count',
+ $db->select( 'journeys', 'count(*) as count' )->hash->{count}
+ )
+ );
+ push(
+ @stats,
+ query_to_influx(
+ 'polyline_count',
+ $db->select( 'polylines', 'count(*) as count' )->hash->{count}
+ )
+ );
+
+ push(
+ @stations,
+ query_to_influx(
+ 'iris',
+ $db->select(
+ 'stations',
+ 'count(*) as count',
+ {
+ source => 0,
+ archived => 0
+ }
+ )->hash->{count}
+ )
+ );
+ push(
+ @stations,
+ query_to_influx(
+ 'hafas',
+ $db->select(
+ 'stations',
+ 'count(*) as count',
+ {
+ source => 1,
+ archived => 0
+ }
+ )->hash->{count}
+ )
+ );
+ push(
+ @stations,
+ query_to_influx(
+ 'archived',
+ $db->select( 'stations', 'count(*) as count', { archived => 1 } )
+ ->hash->{count}
+ )
+ );
+ push(
+ @stations,
+ query_to_influx(
+ 'meta',
+ $db->select( 'related_stations', 'count(*) as count' )
+ ->hash->{count}
+ )
+ );
+
+ push(
+ @traewelling,
+ query_to_influx(
+ 'pull_user_count',
+ $db->select(
+ 'traewelling',
+ 'count(*) as count',
+ { pull_sync => 1 }
+ )->hash->{count}
+ )
+ );
+ push(
+ @traewelling,
+ query_to_influx(
+ 'push_user_count',
+ $db->select(
+ 'traewelling',
+ 'count(*) as count',
+ { push_sync => 1 }
+ )->hash->{count}
+ )
+ );
+ push(
+ @stats,
+ query_to_influx(
+ 'polyline_ratio',
+ $db->query(
+'select (select count(polyline_id) from journeys)::float / (select count(*) from polylines) as ratio'
+ )->hash->{ratio}
+ )
+ );
+
+ if ( $self->app->mode eq 'development' ) {
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . ' stats '
+ . join( ',', @stats ) );
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . ' stations '
+ . join( ',', @stations ) );
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . ' traewelling '
+ . join( ',', @traewelling ) );
+ }
+ elsif ( $self->app->config->{influxdb}->{url} ) {
+ $self->app->ua->post_p(
+ $self->app->config->{influxdb}->{url},
+ 'stats ' . join( ',', @stats )
+ )->wait;
+ $self->app->ua->post_p(
+ $self->app->config->{influxdb}->{url},
+ 'stations ' . join( ',', @stations )
+ )->wait;
+ $self->app->ua->post_p(
+ $self->app->config->{influxdb}->{url},
+ 'traewelling ' . join( ',', @traewelling )
+ )->wait;
+ }
+ else {
+ $self->app->log->warn(
+ "influxdb command called, but no influxdb url has been configured");
+ }
+
+ return;
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl influxdb
+
+ Write statistics to InfluxDB
diff --git a/lib/Travelynx/Command/integritycheck.pm b/lib/Travelynx/Command/integritycheck.pm
new file mode 100644
index 0000000..4894c3d
--- /dev/null
+++ b/lib/Travelynx/Command/integritycheck.pm
@@ -0,0 +1,112 @@
+package Travelynx::Command::integritycheck;
+
+# Copyright (C) 2022 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use Mojo::Base 'Mojolicious::Command';
+use List::Util qw();
+use Travel::Status::DE::IRIS::Stations;
+
+sub run {
+ my ($self) = @_;
+ my $found = 0;
+ my $db = $self->app->pg->db;
+
+ my $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ my $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ my %notified;
+ while ( my $row = $res1->hash ) {
+ my $eva = $row->{checkin_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say
+'Journeys in the travelynx database contain the following unknown EVA IDs.';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ while ( my $row = $res2->hash ) {
+ my $eva = $row->{checkout_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say
+'Journeys in the travelynx database contain the following unknown EVA IDs.';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ if ($found) {
+ say '------------8<----------';
+ say '';
+ $found = 0;
+ }
+
+ my $rename = $self->app->renamed_station;
+
+ my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand;
+ while ( my $j = $res->hash ) {
+ if ( $j->{edited} & 0x0010 ) {
+ next;
+ }
+ my @stops = @{ $j->{route} // [] };
+ for my $stop (@stops) {
+ my $stop_name = $stop->[0];
+ if ( $rename->{ $stop->[0] } ) {
+ $stop->[0] = $rename->{ $stop->[0] };
+ }
+ }
+ my @unknown
+ = $self->app->stations->grep_unknown( map { $_->[0] } @stops );
+ for my $stop_name (@unknown) {
+ if ( not $notified{$stop_name} ) {
+ if ( not $found ) {
+ say
+'Journeys in the travelynx database contain the following unknown route entries.';
+ say 'Note that this check ignores manual route entries.';
+ say 'All reports refer to routes obtained via HAFAS/IRIS.';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ $found = 1;
+ }
+ say $stop_name;
+ $notified{$stop_name} = 1;
+ }
+ }
+ }
+ if ($found) {
+ say '------------8<----------';
+ say '';
+ }
+}
+
+1;
diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm
index 5cbf982..c9c7ed6 100644
--- a/lib/Travelynx/Command/maintenance.pm
+++ b/lib/Travelynx/Command/maintenance.pm
@@ -1,4 +1,8 @@
package Travelynx::Command::maintenance;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use DateTime;
@@ -10,22 +14,15 @@ has usage => sub { shift->extract_usage };
sub run {
my ( $self, $filename ) = @_;
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $checkin_deadline = $now->clone->subtract( hours => 48 );
- my $verification_deadline = $now->clone->subtract( hours => 48 );
- my $deletion_deadline = $now->clone->subtract( hours => 72 );
- my $old_deadline = $now->clone->subtract( years => 1 );
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $verification_deadline = $now->clone->subtract( hours => 48 );
+ my $deletion_deadline = $now->clone->subtract( hours => 72 );
+ my $old_deadline = $now->clone->subtract( years => 1 );
+ my $old_notification_deadline = $now->clone->subtract( weeks => 4 );
my $db = $self->app->pg->db;
my $tx = $db->begin;
- my $res = $db->delete( 'in_transit',
- { checkin_time => { '<', $checkin_deadline } } );
-
- if ( my $rows = $res->rows ) {
- printf( "Removed %d incomplete checkins\n", $rows );
- }
-
my $unverified = $db->select(
'users',
'id, email, extract(epoch from registered_at) as registered_ts',
@@ -72,7 +69,7 @@ sub run {
printf( "Pruned unverified user %d\n", $user->{id} );
}
- $res = $db->delete( 'pending_passwords',
+ my $res = $db->delete( 'pending_passwords',
{ requested_at => { '<', $verification_deadline } } );
if ( my $rows = $res->rows ) {
@@ -86,12 +83,40 @@ sub run {
printf( "Pruned %d pending mail change(s)\n", $rows );
}
+ my $to_notify = $db->select(
+ 'users',
+ [ 'id', 'name', 'email', 'last_seen' ],
+ {
+ last_seen => { '<', $old_deadline },
+ deletion_notified => undef
+ }
+ );
+
+ for my $user ( $to_notify->hashes->each ) {
+ say "Sending account deletion notification to uid $user->{id}...";
+ $self->app->sendmail->age_deletion_notification(
+ name => $user->{name},
+ email => $user->{email},
+ last_seen => $user->{last_seen},
+ login_url => $self->app->base_url_for('login')->to_abs,
+ account_url => $self->app->base_url_for('account')->to_abs,
+ imprint_url => $self->app->base_url_for('impressum')->to_abs,
+ );
+ $self->app->users->mark_deletion_notified( uid => $user->{id} );
+ }
+
my $to_delete = $db->select( 'users', ['id'],
{ deletion_requested => { '<', $deletion_deadline } } );
my @uids_to_delete = $to_delete->arrays->map( sub { shift->[0] } )->each;
- $to_delete
- = $db->select( 'users', ['id'], { last_seen => { '<', $old_deadline } } );
+ $to_delete = $db->select(
+ 'users',
+ ['id'],
+ {
+ last_seen => { '<', $old_deadline },
+ deletion_notified => { '<', $old_notification_deadline }
+ }
+ );
push( @uids_to_delete,
$to_delete->arrays->map( sub { shift->[0] } )->each );
@@ -101,33 +126,49 @@ sub run {
"About to delete %d accounts, which is quite a lot.\n",
scalar @uids_to_delete
);
+ for my $uid (@uids_to_delete) {
+ my $journeys_res = $db->select(
+ 'journeys',
+ 'count(*) as count',
+ { user_id => $uid }
+ )->hash;
+ printf STDERR (
+ " - UID %5d (%4d journeys)\n",
+ $uid, $journeys_res->{count}
+ );
+ }
say STDERR 'Aborting maintenance. Please investigate.';
exit(1);
}
for my $uid (@uids_to_delete) {
say "Deleting uid ${uid}...";
- my $tokens_res = $db->delete( 'tokens', { user_id => $uid } );
- my $stats_res = $db->delete( 'journey_stats', { user_id => $uid } );
- my $journeys_res = $db->delete( 'journeys', { user_id => $uid } );
- my $transit_res = $db->delete( 'in_transit', { user_id => $uid } );
- my $password_res
- = $db->delete( 'pending_passwords', { user_id => $uid } );
- my $user_res = $db->delete( 'users', { id => $uid } );
-
+ my $count = $self->app->users->delete(
+ uid => $uid,
+ db => $db,
+ in_transaction => 1
+ );
printf( " %d tokens, %d monthly stats, %d journeys\n",
- $tokens_res->rows, $stats_res->rows, $journeys_res->rows );
-
- if ( $user_res->rows != 1 ) {
- printf STDERR (
- "Deleted %d rows from users, expected 1. Rollback and abort.\n",
- $user_res->rows
- );
- exit(1);
- }
+ $count->{tokens}, $count->{stats}, $count->{journeys} );
}
$tx->commit;
+
+ # Computing stats may take a while, but we've got all time in the
+ # world here. This means users won't have to wait when loading their
+ # own journey log.
+ say 'Generating missing stats ...';
+ for
+ my $user ( $db->select( 'users', ['id'], { status => 1 } )->hashes->each )
+ {
+ $tx = $db->begin;
+ $self->app->journeys->generate_missing_stats( uid => $user->{id} );
+ $self->app->journeys->get_stats(
+ uid => $user->{id},
+ year => $now->year
+ );
+ $tx->commit;
+ }
}
1;
diff --git a/lib/Travelynx/Command/munin.pm b/lib/Travelynx/Command/munin.pm
index ee509d3..3b6e393 100644
--- a/lib/Travelynx/Command/munin.pm
+++ b/lib/Travelynx/Command/munin.pm
@@ -1,4 +1,8 @@
package Travelynx::Command::munin;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use DateTime;
@@ -11,7 +15,7 @@ sub query_to_munin {
my ( $label, $value ) = @_;
if ( defined $value ) {
- printf( "%s.value %d\n", $label, $value );
+ printf( "%s.value %f\n", $label, $value );
}
}
@@ -26,6 +30,19 @@ sub run {
my $checkin_window_query
= qq{select count(*) as count from journeys where checkin_time > to_timestamp(?);};
+ # DateTime's math does not like time zones: When subtracting 7 days from
+ # sun 2am and the previous sunday was the switch from CET to CEST (i.e.,
+ # the switch to daylight saving time), the resulting datetime is invalid.
+ # This is a fatal error. We avoid this edge case by performing date math
+ # on the epoch timestamp, which does not know or care about time zones and
+ # daylight saving time.
+ my $one_day = 24 * 60 * 60;
+ my $one_week = 7 * $one_day;
+ my $one_month = 30 * $one_day;
+
+ query_to_munin( 'pending_user_count',
+ $db->select( 'users', 'count(*) as count', { status => 0 } )
+ ->hash->{count} );
query_to_munin( 'reg_user_count',
$db->select( 'users', 'count(*) as count', { status => 1 } )
->hash->{count} );
@@ -42,19 +59,28 @@ sub run {
);
query_to_munin( 'checked_in',
$db->select( 'in_transit', 'count(*) as count' )->hash->{count} );
- query_to_munin(
- 'checkins_24h',
- $db->query( $checkin_window_query,
- $now->subtract( hours => 24 )->epoch )->hash->{count}
- );
+ query_to_munin( 'checkins_24h',
+ $db->query( $checkin_window_query, $now->epoch - $one_day )
+ ->hash->{count} );
query_to_munin( 'checkins_7d',
- $db->query( $checkin_window_query, $now->subtract( days => 7 )->epoch )
+ $db->query( $checkin_window_query, $now->epoch - $one_week )
+ ->hash->{count} );
+ query_to_munin( 'checkins_30d',
+ $db->query( $checkin_window_query, $now->epoch - $one_month )
+ ->hash->{count} );
+ query_to_munin( 'polylines',
+ $db->select( 'polylines', 'count(*) as count' )->hash->{count} );
+ query_to_munin( 'traewelling_pull',
+ $db->select( 'traewelling', 'count(*) as count', { pull_sync => 1 } )
+ ->hash->{count} );
+ query_to_munin( 'traewelling_push',
+ $db->select( 'traewelling', 'count(*) as count', { push_sync => 1 } )
->hash->{count} );
query_to_munin(
- 'checkins_30d',
+ 'polyline_ratio',
$db->query(
- $checkin_window_query, $now->subtract( days => 30 )->epoch
- )->hash->{count}
+'select (select count(polyline_id) from journeys)::float / (select count(*) from polylines) as ratio'
+ )->hash->{ratio}
);
}
diff --git a/lib/Travelynx/Command/traewelling.pm b/lib/Travelynx/Command/traewelling.pm
new file mode 100644
index 0000000..4c47e84
--- /dev/null
+++ b/lib/Travelynx/Command/traewelling.pm
@@ -0,0 +1,207 @@
+package Travelynx::Command::traewelling;
+
+# Copyright (C) 2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Command';
+use Mojo::Promise;
+
+use DateTime;
+use JSON;
+use List::Util;
+
+has description => 'Synchronize with Traewelling';
+
+has usage => sub { shift->extract_usage };
+
+sub pull_sync {
+ my ($self) = @_;
+ my %pull_result;
+ my $request_count = 0;
+ for my $account_data ( $self->app->traewelling->get_pull_accounts ) {
+
+ my $in_transit = $self->app->in_transit->get(
+ uid => $account_data->{user_id},
+ );
+ if ($in_transit) {
+ $self->app->log->debug(
+"Skipping Traewelling status pull for UID $account_data->{user_id}: already checked in"
+ );
+ next;
+ }
+
+ # $account_data->{user_id} is the travelynx uid
+ # $account_data->{user_name} is the Träwelling username
+ $request_count += 1;
+ $self->app->log->debug(
+"Scheduling Traewelling status pull for UID $account_data->{user_id}"
+ );
+
+ # In 'work', the event loop is not running,
+ # so there's no need to multiply by $request_count at the moment
+ Mojo::Promise->timer(1)->then(
+ sub {
+ return $self->app->traewelling_api->get_status_p(
+ username => $account_data->{data}{user_name},
+ token => $account_data->{token}
+ );
+ }
+ )->then(
+ sub {
+ my ($traewelling) = @_;
+ $pull_result{ $traewelling->{http} } += 1;
+ return $self->app->traewelling_to_travelynx_p(
+ traewelling => $traewelling,
+ user_data => $account_data
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $pull_result{ $err->{http} // 0 } += 1;
+ $self->app->traewelling->log(
+ uid => $account_data->{user_id},
+ message => "Fehler bei der Status-Abfrage: $err->{text}",
+ is_error => 1
+ );
+ $self->app->log->debug("Error $err->{text}");
+ }
+ )->wait;
+ }
+
+ return \%pull_result;
+}
+
+sub push_sync {
+ my ($self) = @_;
+ my %push_result;
+
+ for my $candidate ( $self->app->traewelling->get_pushable_accounts ) {
+ $self->app->log->debug(
+ "Pushing to Traewelling for UID $candidate->{uid}");
+ my $trip_id = $candidate->{journey_data}{trip_id};
+ if ( not $trip_id ) {
+ $self->app->log->debug("... trip_id is missing");
+ $self->app->traewelling->log(
+ uid => $candidate->{uid},
+ message =>
+"Konnte $candidate->{train_type} $candidate->{train_no} nicht übertragen: Keine trip_id vorhanden",
+ is_error => 1
+ );
+ next;
+ }
+ if ( $candidate->{data}{latest_push_ts}
+ and $candidate->{data}{latest_push_ts} == $candidate->{checkin_ts} )
+ {
+ $self->app->log->debug("... already handled");
+ next;
+ }
+ $self->app->traewelling_api->checkin_p( %{$candidate},
+ trip_id => $trip_id )->then(
+ sub {
+ my ($status) = @_;
+ $push_result{ $status->{http} } += 1;
+ }
+ )->catch(
+ sub {
+ my ($status) = @_;
+ $push_result{ $status->{http} // 0 } += 1;
+ }
+ )->wait;
+ }
+
+ return \%push_result;
+}
+
+sub run {
+ my ( $self, $direction ) = @_;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $started_at = $now;
+ my $push_result;
+ my $pull_result;
+
+ if ( not $direction or $direction eq 'push' ) {
+ $push_result = $self->push_sync;
+ }
+
+ my $trwl_push_finished_at = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ if ( not $direction or $direction eq 'pull' ) {
+ $pull_result = $self->pull_sync;
+ }
+
+ my $trwl_pull_finished_at = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my $trwl_push_duration = $trwl_push_finished_at->epoch - $started_at->epoch;
+ my $trwl_pull_duration
+ = $trwl_pull_finished_at->epoch - $trwl_push_finished_at->epoch;
+ my $trwl_duration = $trwl_pull_finished_at->epoch - $started_at->epoch;
+
+ if ( $self->app->config->{influxdb}->{url} ) {
+ my $report = "sync_runtime_seconds=${trwl_duration}";
+ if ( not $direction or $direction eq 'push' ) {
+ $report .= ",push_runtime_seconds=${trwl_push_duration}";
+ }
+ if ( not $direction or $direction eq 'pull' ) {
+ $report .= ",pull_runtime_seconds=${trwl_pull_duration}";
+ }
+ if ( $self->app->mode eq 'development' ) {
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . " traewelling ${report}" );
+ }
+ else {
+ $self->app->ua->post_p( $self->app->config->{influxdb}->{url},
+ "traewelling ${report}" )->wait;
+ }
+
+ if ($push_result) {
+ for my $status ( keys %{$push_result} ) {
+ my $count = $push_result->{$status};
+ if ( $self->app->mode eq 'development' ) {
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . " traewelling_push,http=$status count=$count" );
+ }
+ else {
+ $self->app->ua->post_p(
+ $self->app->config->{influxdb}->{url},
+ "traewelling_push,http=$status count=$count"
+ )->wait;
+ }
+ }
+ }
+
+ if ($pull_result) {
+ for my $status ( keys %{$pull_result} ) {
+ my $count = $pull_result->{$status};
+ if ( $self->app->mode eq 'development' ) {
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . " traewelling_pull,http=$status count=$count" );
+ }
+ else {
+ $self->app->ua->post_p(
+ $self->app->config->{influxdb}->{url},
+ "traewelling_pull,http=$status count=$count"
+ )->wait;
+ }
+ }
+ }
+ }
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl traewelling [direction]
+
+ Performs both push and pull synchronization by default.
+ If "direction" is specified, only synchronizes in the specified direction
+ ("push" or "pull")
+
+ Should be called from a cronjob every three to ten minutes.
diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm
index fbbf958..10b1b69 100644
--- a/lib/Travelynx/Command/work.pm
+++ b/lib/Travelynx/Command/work.pm
@@ -1,32 +1,114 @@
package Travelynx::Command::work;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
+use Mojo::Promise;
use DateTime;
use JSON;
use List::Util;
-has description =>
- 'Perform automatic checkout when users arrive at their destination';
+has description => 'Update real-time data of active journeys';
has usage => sub { shift->extract_usage };
sub run {
my ($self) = @_;
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $json = JSON->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $checkin_deadline = $now->clone->subtract( hours => 48 );
+ my $json = JSON->new;
+
+ my $num_incomplete = $self->app->in_transit->delete_incomplete_checkins(
+ earlier_than => $checkin_deadline );
- my $db = $self->app->pg->db;
+ if ($num_incomplete) {
+ $self->app->log->debug("Removed ${num_incomplete} incomplete checkins");
+ }
- for my $entry (
- $db->select( 'in_transit_str', '*', { cancelled => 0 } )->hashes->each )
- {
+ my $errors = 0;
+
+ for my $entry ( $self->app->in_transit->get_all_active ) {
my $uid = $entry->{user_id};
my $dep = $entry->{dep_eva};
my $arr = $entry->{arr_eva};
my $train_id = $entry->{train_id};
+ if ( $train_id =~ m{[|]} ) {
+
+ $self->app->hafas->get_journey_p( trip_id => $train_id )->then(
+ sub {
+ my ($journey) = @_;
+
+ my $found_dep;
+ my $found_arr;
+ for my $stop ( $journey->route ) {
+ if ( $stop->loc->eva == $dep ) {
+ $found_dep = $stop;
+ }
+ if ( $arr and $stop->loc->eva == $arr ) {
+ $found_arr = $stop;
+ last;
+ }
+ }
+ if ( not $found_dep ) {
+ $self->app->log->debug(
+ "Did not find $dep within journey $train_id");
+ return;
+ }
+
+ if ( $found_dep->{rt_dep} ) {
+ $self->app->in_transit->update_departure_hafas(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_dep,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
+
+ if ( $found_arr and $found_arr->{rt_arr} ) {
+ $self->app->in_transit->update_arrival_hafas(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_arr,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ if ( $err =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$} )
+ {
+ # HAFAS do be weird. These are not actionable.
+ $self->app->log->debug("work($uid)/journey: $err");
+ }
+ else {
+ $self->app->log->error("work($uid)/journey: $err");
+ }
+ }
+ )->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ next;
+ }
+
# Note: IRIS data is not always updated in real-time. Both departure and
# arrival delays may take several minutes to appear, especially in case
# of large-scale disturbances. We work around this by continuing to
@@ -35,7 +117,11 @@ sub run {
eval {
if ( $now->epoch - $entry->{real_dep_ts} < 900 ) {
- my $status = $self->app->get_departures( $dep, 30, 30 );
+ my $status = $self->app->iris->get_departures(
+ station => $dep,
+ lookbehind => 30,
+ lookahead => 30
+ );
if ( $status->{errstr} ) {
die("get_departures($dep): $status->{errstr}\n");
}
@@ -44,29 +130,52 @@ sub run {
@{ $status->{results} };
if ( not $train ) {
- die("could not find train $train_id at $dep\n");
+ $self->app->log->debug(
+ "could not find train $train_id at $dep\n");
+ return;
}
- $db->update(
- 'in_transit',
- {
- dep_platform => $train->platform,
- real_departure => $train->departure,
- route =>
- $json->encode( [ $self->app->route_diff($train) ] ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- ),
- },
- { user_id => $uid }
+ $self->app->in_transit->update_departure(
+ uid => $uid,
+ train => $train,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ route => [ $self->app->iris->route_diff($train) ]
);
- $self->app->add_route_timestamps( $uid, $train, 1 );
+
+ 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,
+ );
+
+ # 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 );
+ }
}
};
if ($@) {
+ $errors += 1;
$self->app->log->error("work($uid)/departure: $@");
}
@@ -77,7 +186,11 @@ sub run {
or $now->epoch - $entry->{real_arr_ts} < 600 )
)
{
- my $status = $self->app->get_departures( $arr, 20, 220 );
+ my $status = $self->app->iris->get_departures(
+ station => $arr,
+ lookbehind => 20,
+ lookahead => 220
+ );
if ( $status->{errstr} ) {
die("get_departures($arr): $status->{errstr}\n");
}
@@ -102,48 +215,80 @@ sub run {
return;
}
- $db->update(
- 'in_transit',
- {
- arr_platform => $train->platform,
- sched_arrival => $train->sched_arrival,
- real_arrival => $train->arrival,
- route =>
- $json->encode( [ $self->app->route_diff($train) ] ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- ),
- },
- { user_id => $uid }
+ 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,
);
- $self->app->add_route_timestamps( $uid, $train, 0 );
+
+ 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(
+ 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
+ );
+ }
}
elsif ( $entry->{real_arr_ts} ) {
- my ( undef, $error ) = $self->app->checkout( $arr, 1, $uid );
- if ($error) {
- die("${error}\n");
- }
+ my ( undef, $error ) = $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->app->log->error("work($uid)/arrival: $error");
+ $errors += 1;
+ }
+ )->wait;
}
};
if ($@) {
$self->app->log->error("work($uid)/arrival: $@");
+ $errors += 1;
}
- eval { }
+ eval { };
+ }
+
+ my $started_at = $now;
+ my $main_finished_at = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $worker_duration = $main_finished_at->epoch - $started_at->epoch;
+
+ if ( $self->app->config->{influxdb}->{url} ) {
+ if ( $self->app->mode eq 'development' ) {
+ $self->app->log->debug( 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . " worker runtime_seconds=${worker_duration},errors=${errors}"
+ );
+ }
+ else {
+ $self->app->ua->post_p( $self->app->config->{influxdb}->{url},
+ "worker runtime_seconds=${worker_duration},errors=${errors}" )
+ ->wait;
+ }
}
- # Computing yearly stats may take a while, but we've got all time in the
- # world here. This means users won't have to wait when loading their
- # own by-year journey log.
- for my $user ( $db->select( 'users', 'id', { status => 1 } )->hashes->each )
- {
- $self->app->get_journey_stats(
- uid => $user->{id},
- year => $now->year
- );
+ if ( not $self->app->config->{traewelling}->{separate_worker} ) {
+ $self->app->start('traewelling');
}
}
diff --git a/lib/Travelynx/Command/worker.pm b/lib/Travelynx/Command/worker.pm
index 6b70f2e..be7431f 100644
--- a/lib/Travelynx/Command/worker.pm
+++ b/lib/Travelynx/Command/worker.pm
@@ -1,24 +1,31 @@
package Travelynx::Command::worker;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use Mojo::IOLoop;
-has description =>
- 'travelynx background worker';
+has description => 'travelynx background worker';
has usage => sub { shift->extract_usage };
sub run {
my ($self) = @_;
- Mojo::IOLoop->recurring(180 => sub {
- $self->app->start('work');
- });
+ Mojo::IOLoop->recurring(
+ 180 => sub {
+ $self->app->start('work');
+ }
+ );
- Mojo::IOLoop->recurring(3600 => sub {
- $self->app->start('maintenance');
- });
+ Mojo::IOLoop->recurring(
+ 36000 => sub {
+ $self->app->start('maintenance');
+ }
+ );
- if (not Mojo::IOLoop->is_running) {
+ if ( not Mojo::IOLoop->is_running ) {
Mojo::IOLoop->start;
}
}
@@ -33,4 +40,4 @@ __END__
Background worker for cron-less setups, e.g. Docker.
- Calls "index.pl work" every 3 minutes and "index.pl maintenance" every 1 hour.
+ Calls "index.pl work" every 3 minutes and "index.pl maintenance" every 10 hours.
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index cef79a5..f1dc43e 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1,21 +1,228 @@
package Travelynx::Controller::Account;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
-use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
+use JSON;
+use Mojo::Util qw(xml_escape);
+use Text::Markdown;
use UUID::Tiny qw(:std);
-sub hash_password {
- my ($password) = @_;
- my @salt_bytes = map { int( rand(255) ) + 1 } ( 1 .. 16 );
- my $salt = en_base64( pack( 'C[16]', @salt_bytes ) );
+my %visibility_itoa = (
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+);
- return bcrypt( $password, '$2a$12$' . $salt );
-}
+my %visibility_atoi = (
+ public => 100,
+ travelynx => 80,
+ followers => 60,
+ unlisted => 30,
+ private => 10,
+);
+
+# Internal Helpers
sub make_token {
return create_uuid_as_string(UUID_V4);
}
+sub send_registration_mail {
+ my ( $self, %opt ) = @_;
+
+ my $email = $opt{email};
+ my $token = $opt{token};
+ my $user = $opt{user};
+ my $user_id = $opt{user_id};
+ my $ip = $opt{ip};
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ my $ua = $self->req->headers->user_agent;
+ my $reg_url = $self->url_for('reg')->to_abs->scheme('https');
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo, ${user}!\n\n";
+ $body .= "Mit deiner E-Mail-Adresse (${email}) wurde ein Account bei\n";
+ $body .= "travelynx angelegt.\n\n";
+ $body
+ .= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
+ $body .= "${reg_url}/${user_id}/${token}\n";
+ $body .= "freischalten.\n\n";
+ $body
+ .= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n";
+ $body
+ .= "Mail-Adresse erneut zur Registrierung freigeschaltet. Falls auch diese fehlschlägt,\n";
+ $body
+ .= "werden wir sie dauerhaft sperren und keine Mails mehr dorthin schicken.\n\n";
+ $body .= "Daten zur Registrierung:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->sendmail->custom( $email, 'Registrierung bei travelynx',
+ $body );
+}
+
+sub send_address_confirmation_mail {
+ my ( $self, $email, $token ) = @_;
+
+ my $name = $self->current_user->{name};
+ my $ip = $self->req->headers->header('X-Forwarded-For');
+ my $ua = $self->req->headers->user_agent;
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ # In case Mojolicious is not running behind a reverse proxy
+ $ip
+ //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
+ my $confirm_url = $self->url_for('confirm_mail')->to_abs->scheme('https');
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo ${name},\n\n";
+ $body .= "Bitte bestätige unter <${confirm_url}/${token}>,\n";
+ $body .= "dass du mit dieser Adresse E-Mail empfangen kannst.\n\n";
+ $body
+ .= "Du erhältst diese Mail, da eine Änderung der deinem travelynx-Account\n";
+ $body .= "zugeordneten Mail-Adresse beantragt wurde.\n\n";
+ $body .= "Daten zur Anfrage:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->sendmail->custom( $email,
+ 'travelynx: Mail-Adresse bestätigen', $body );
+}
+
+sub send_name_notification_mail {
+ my ( $self, $old_name, $new_name ) = @_;
+
+ my $ip = $self->req->headers->header('X-Forwarded-For');
+ my $ua = $self->req->headers->user_agent;
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ # In case Mojolicious is not running behind a reverse proxy
+ $ip
+ //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
+ my $confirm_url = $self->url_for('confirm_mail')->to_abs->scheme('https');
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo ${new_name},\n\n";
+ $body .= "Der Name deines Travelynx-Accounts wurde erfolgreich geändert.\n";
+ $body
+ .= "Bitte beachte, dass du dich ab sofort nur mit dem neuen Namen anmelden kannst.\n\n";
+ $body .= "Alter Name: ${old_name}\n\n";
+ $body .= "Neue Name: ${new_name}\n\n";
+ $body .= "Daten zur Anfrage:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->sendmail->custom( $self->current_user->{email},
+ 'travelynx: Name geändert', $body );
+}
+
+sub send_password_notification_mail {
+ my ($self) = @_;
+ my $user = $self->current_user->{name};
+ my $email = $self->current_user->{email};
+ my $ip = $self->req->headers->header('X-Forwarded-For');
+ my $ua = $self->req->headers->user_agent;
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ # In case Mojolicious is not running behind a reverse proxy
+ $ip
+ //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo ${user},\n\n";
+ $body
+ .= "Das Passwort deines travelynx-Accounts wurde soeben geändert.\n\n";
+ $body .= "Daten zur Änderung:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ $self->sendmail->custom( $email, 'travelynx: Passwort geändert', $body );
+}
+
+sub send_lostpassword_confirmation_mail {
+ my ( $self, %opt ) = @_;
+ my $email = $opt{email};
+ my $name = $opt{name};
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ my $ip = $self->req->headers->header('X-Forwarded-For');
+ my $ua = $self->req->headers->user_agent;
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ # In case Mojolicious is not running behind a reverse proxy
+ $ip
+ //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
+ my $recover_url = $self->url_for('recover')->to_abs->scheme('https');
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo ${name},\n\n";
+ $body .= "Unter ${recover_url}/${uid}/${token}\n";
+ $body
+ .= "kannst du ein neues Passwort für deinen travelynx-Account vergeben.\n\n";
+ $body
+ .= "Du erhältst diese Mail, da mit deinem Accountnamen und deiner Mail-Adresse\n";
+ $body
+ .= "ein Passwort-Reset angefordert wurde. Falls diese Anfrage nicht von dir\n";
+ $body .= "ausging, kannst du sie ignorieren.\n\n";
+ $body .= "Daten zur Anfrage:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ my $success
+ = $self->sendmail->custom( $email, 'travelynx: Neues Passwort', $body );
+}
+
+sub send_lostpassword_notification_mail {
+ my ( $self, $account ) = @_;
+ my $user = $account->{name};
+ my $email = $account->{email};
+ my $ip = $self->req->headers->header('X-Forwarded-For');
+ my $ua = $self->req->headers->user_agent;
+ my $date = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M:%S %z');
+
+ # In case Mojolicious is not running behind a reverse proxy
+ $ip
+ //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
+ my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+
+ my $body = "Hallo ${user},\n\n";
+ $body .= "Das Passwort deines travelynx-Accounts wurde soeben über die";
+ $body .= " 'Passwort vergessen'-Funktion geändert.\n\n";
+ $body .= "Daten zur Änderung:\n";
+ $body .= " * Datum: ${date}\n";
+ $body .= " * Client: ${ip}\n";
+ $body .= " * UserAgent: ${ua}\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->sendmail->custom( $email, 'travelynx: Passwort geändert',
+ $body );
+}
+
+# Controllers
+
sub login_form {
my ($self) = @_;
$self->render('login');
@@ -31,22 +238,31 @@ sub do_login {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'login',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
}
else {
if ( $self->authenticate( $user, $password ) ) {
$self->redirect_to( $self->req->param('redirect_to') // '/' );
- $self->mark_seen( $self->current_user->{id} );
+ $self->users->mark_seen( uid => $self->current_user->{id} );
}
else {
- my $data = $self->get_user_password($user);
+ my $data = $self->users->get_login_data( name => $user );
if ( $data and $data->{status} == 0 ) {
- $self->render( 'login', invalid => 'confirmation' );
+ $self->render(
+ 'login',
+ status => 400,
+ invalid => 'confirmation'
+ );
}
else {
- $self->render( 'login', invalid => 'credentials' );
+ $self->render(
+ 'login',
+ status => 400,
+ invalid => 'credentials'
+ );
}
}
}
@@ -59,14 +275,12 @@ sub registration_form {
sub register {
my ($self) = @_;
+ my $dt = $self->req->param('dt');
my $user = $self->req->param('user');
my $email = $self->req->param('email');
my $password = $self->req->param('password');
my $password2 = $self->req->param('password2');
my $ip = $self->req->headers->header('X-Forwarded-For');
- my $ua = $self->req->headers->user_agent;
- my $date = DateTime->now( time_zone => 'Europe/Berlin' )
- ->strftime('%d.%m.%Y %H:%M:%S %z');
# In case Mojolicious is not running behind a reverse proxy
$ip
@@ -74,33 +288,44 @@ sub register {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'register',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
- if ( not length($user) ) {
- $self->render( 'register', invalid => 'user_empty' );
- return;
- }
-
- if ( not length($email) ) {
- $self->render( 'register', invalid => 'mail_empty' );
- return;
+ if ( my $registration_denylist
+ = $self->app->config->{registration}->{denylist} )
+ {
+ if ( open( my $fh, "<", $registration_denylist ) ) {
+ while ( my $line = <$fh> ) {
+ chomp $line;
+ if ( $ip eq $line ) {
+ close($fh);
+ $self->render( 'register', invalid => "denylist" );
+ return;
+ }
+ }
+ close($fh);
+ }
+ else {
+ $self->log->error("Cannot open($registration_denylist): $!");
+ die("Cannot verify registration: $!");
+ }
}
- if ( $user !~ m{ ^ [0-9a-zA-Z_-]+ $ }x ) {
- $self->render( 'register', invalid => 'user_format' );
+ if ( my $error = $self->users->is_name_invalid( name => $user ) ) {
+ $self->render( 'register', invalid => $error );
return;
}
- if ( $self->check_if_user_name_exists($user) ) {
- $self->render( 'register', invalid => 'user_collision' );
+ if ( not length($email) ) {
+ $self->render( 'register', invalid => 'mail_empty' );
return;
}
- if ( $self->check_if_mail_is_blacklisted($email) ) {
+ if ( $self->users->mail_is_blacklisted( email => $email ) ) {
$self->render( 'register', invalid => 'mail_blacklisted' );
return;
}
@@ -115,35 +340,37 @@ sub register {
return;
}
- my $token = make_token();
- my $pw_hash = hash_password($password);
- my $db = $self->pg->db;
- my $tx = $db->begin;
- my $user_id = $self->add_user( $db, $user, $email, $token, $pw_hash );
- my $reg_url = $self->url_for('reg')->to_abs->scheme('https');
- my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
+ if ( not $dt
+ or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 6 )
+ {
+ # a human user should take at least five seconds to fill out the form.
+ # Throw a CSRF error at presumed spammers.
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
- my $body = "Hallo, ${user}!\n\n";
- $body .= "Mit deiner E-Mail-Adresse (${email}) wurde ein Account bei\n";
- $body .= "travelynx angelegt.\n\n";
- $body
- .= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
- $body .= "${reg_url}/${user_id}/${token}\n";
- $body .= "freischalten.\n\n";
- $body
- .= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n";
- $body
- .= "Mail-Adresse erneut zur Registrierung freigeschaltet. Falls auch diese fehlschlägt,\n";
- $body
- .= "werden wir sie dauerhaft sperren und keine Mails mehr dorthin schicken.\n\n";
- $body .= "Daten zur Registrierung:\n";
- $body .= " * Datum: ${date}\n";
- $body .= " * Client: ${ip}\n";
- $body .= " * UserAgent: ${ua}\n\n\n";
- $body .= "Impressum: ${imprint_url}\n";
+ my $token = make_token();
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+ my $user_id = $self->users->add(
+ db => $db,
+ name => $user,
+ email => $email,
+ token => $token,
+ password => $password,
+ );
- my $success
- = $self->sendmail->custom( $email, 'Registrierung bei travelynx', $body );
+ my $success = $self->send_registration_mail(
+ email => $email,
+ token => $token,
+ ip => $ip,
+ user => $user,
+ user_id => $user_id
+ );
if ($success) {
$tx->commit;
$self->render( 'login', from => 'register' );
@@ -164,7 +391,13 @@ sub verify {
return;
}
- if ( not $self->verify_registration_token( $id, $token ) ) {
+ if (
+ not $self->users->verify_registration_token(
+ uid => $id,
+ token => $token
+ )
+ )
+ {
$self->render( 'register', invalid => 'token' );
return;
}
@@ -174,8 +407,13 @@ sub verify {
sub delete {
my ($self) = @_;
+ my $uid = $self->current_user->{id};
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'account', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
@@ -187,13 +425,14 @@ sub delete {
)
)
{
- $self->render( 'account', invalid => 'deletion password' );
+ $self->flash( invalid => 'deletion password' );
+ $self->redirect_to('account');
return;
}
- $self->flag_user_deletion( $self->current_user->{id} );
+ $self->users->flag_deletion( uid => $uid );
}
else {
- $self->unflag_user_deletion( $self->current_user->{id} );
+ $self->users->unflag_deletion( uid => $uid );
}
$self->redirect_to('account');
}
@@ -201,7 +440,11 @@ sub delete {
sub do_logout {
my ($self) = @_;
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'login', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
$self->logout;
@@ -211,52 +454,353 @@ sub do_logout {
sub privacy {
my ($self) = @_;
- my $user = $self->current_user;
- my $public_level = $user->{is_public};
+ my $user = $self->current_user;
if ( $self->param('action') and $self->param('action') eq 'save' ) {
- if ( $self->param('status_level') eq 'intern' ) {
- $public_level |= 0x01;
- $public_level &= ~0x02;
+ my %opt;
+ my $default_visibility
+ = $visibility_atoi{ $self->param('status_level') };
+ if ( defined $default_visibility ) {
+ $opt{default_visibility} = $default_visibility;
}
- elsif ( $self->param('status_level') eq 'extern' ) {
- $public_level |= 0x02;
- $public_level &= ~0x01;
+
+ my $past_visibility = $visibility_atoi{ $self->param('history_level') };
+ if ( defined $past_visibility ) {
+ $opt{past_visibility} = $past_visibility;
}
- else {
- $public_level &= ~0x03;
+
+ $opt{comments_visible} = $self->param('public_comment') ? 1 : 0;
+
+ $opt{past_all} = $self->param('history_age') eq 'infinite' ? 1 : 0;
+ $opt{past_status} = $self->param('past_status') ? 1 : 0;
+
+ $self->users->set_privacy(
+ uid => $user->{id},
+ %opt
+ );
+
+ $self->flash( success => 'privacy' );
+ $self->redirect_to('account');
+ }
+ else {
+ $self->param(
+ status_level => $visibility_itoa{ $user->{default_visibility} } );
+ $self->param( public_comment => $user->{comments_visible} );
+ $self->param(
+ history_level => $visibility_itoa{ $user->{past_visibility} } );
+ $self->param( history_age => $user->{past_all} ? 'infinite' : 'month' );
+ $self->param( past_status => $user->{past_status} );
+ $self->render( 'privacy', name => $user->{name} );
+ }
+}
+
+sub social {
+ my ($self) = @_;
+
+ my $user = $self->current_user;
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
}
- # public comment with non-public status does not make sense
- if ( $self->param('public_comment')
- and $self->param('status_level') ne 'private' )
- {
- $public_level |= 0x04;
+ my %opt;
+ my $accept_follow = $self->param('accept_follow');
+
+ if ( $accept_follow eq 'yes' ) {
+ $opt{accept_follows} = 1;
}
- else {
- $public_level &= ~0x04;
+ elsif ( $accept_follow eq 'request' ) {
+ $opt{accept_follow_requests} = 1;
}
- $self->set_privacy( $user->{id}, $public_level );
- $self->flash( success => 'privacy' );
+ $self->users->set_social(
+ uid => $user->{id},
+ %opt
+ );
+
+ $self->flash( success => 'social' );
$self->redirect_to('account');
}
else {
- $self->param(
- status_level => $public_level & 0x01 ? 'intern'
- : $public_level & 0x02 ? 'extern'
- : 'private'
+ if ( $user->{accept_follows} ) {
+ $self->param( accept_follow => 'yes' );
+ }
+ elsif ( $user->{accept_follow_requests} ) {
+ $self->param( accept_follow => 'request' );
+ }
+ else {
+ $self->param( accept_follow => 'no' );
+ }
+ $self->render( 'social', name => $user->{name} );
+ }
+}
+
+sub social_list {
+ my ($self) = @_;
+
+ my $kind = $self->stash('kind');
+ my $user = $self->current_user;
+
+ if ( $kind eq 'follow-requests-received' ) {
+ my @follow_reqs
+ = $self->users->get_follow_requests( uid => $user->{id} );
+ $self->render(
+ 'social_list',
+ type => 'follow-requests-received',
+ entries => [@follow_reqs],
+ notifications => $user->{notifications},
);
- $self->param( public_comment => $public_level & 0x04 ? 1 : 0 );
- $self->render( 'privacy', name => $user->{name} );
+ }
+ elsif ( $kind eq 'follow-requests-sent' ) {
+ my @follow_reqs = $self->users->get_follow_requests(
+ uid => $user->{id},
+ sent => 1
+ );
+ $self->render(
+ 'social_list',
+ type => 'follow-requests-sent',
+ entries => [@follow_reqs],
+ notifications => $user->{notifications},
+ );
+ }
+ elsif ( $kind eq 'followers' ) {
+ my @followers = $self->users->get_followers( uid => $user->{id} );
+ $self->render(
+ 'social_list',
+ type => 'followers',
+ entries => [@followers],
+ notifications => $user->{notifications},
+ );
+ }
+ elsif ( $kind eq 'follows' ) {
+ my @following = $self->users->get_followees( uid => $user->{id} );
+ $self->render(
+ 'social_list',
+ type => 'follows',
+ entries => [@following],
+ notifications => $user->{notifications},
+ );
+ }
+ elsif ( $kind eq 'blocks' ) {
+ my @blocked = $self->users->get_blocked_users( uid => $user->{id} );
+ $self->render(
+ 'social_list',
+ type => 'blocks',
+ entries => [@blocked],
+ notifications => $user->{notifications},
+ );
+ }
+ else {
+ $self->render( 'not_found', status => 404 );
}
}
+sub social_action {
+ my ($self) = @_;
+
+ my $user = $self->current_user;
+ my $action = $self->param('action');
+ my $target_ids = $self->param('target');
+ my $redirect_to = $self->param('redirect_to');
+
+ for my $key (
+ qw(follow request_follow follow_or_request unfollow remove_follower cancel_follow_request accept_follow_request reject_follow_request block unblock)
+ )
+ {
+ if ( $self->param($key) ) {
+ $action = $key;
+ $target_ids = $self->param($key);
+ }
+ }
+
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->redirect_to('/');
+ return;
+ }
+
+ if ( $action and $action eq 'clear_notifications' ) {
+ $self->users->update_notifications(
+ db => $self->pg->db,
+ uid => $user->{id},
+ has_follow_requests => 0
+ );
+ $self->flash( success => 'clear_notifications' );
+ $self->redirect_to('account');
+ return;
+ }
+
+ if ( not( $action and $target_ids and $redirect_to ) ) {
+ $self->redirect_to('/');
+ return;
+ }
+
+ for my $target_id ( split( qr{,}, $target_ids ) ) {
+ my $target = $self->users->get_privacy_by( uid => $target_id );
+
+ if ( not $target ) {
+ next;
+ }
+
+ if ( $action eq 'follow' and $target->{accept_follows} ) {
+ $self->users->follow(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $action eq 'request_follow'
+ and $target->{accept_follow_requests} )
+ {
+ $self->users->request_follow(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $action eq 'follow_or_request' ) {
+ if ( $target->{accept_follows} ) {
+ $self->users->follow(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $target->{accept_follow_requests} ) {
+ $self->users->request_follow(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ }
+ elsif ( $action eq 'unfollow' ) {
+ $self->users->unfollow(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $action eq 'remove_follower' ) {
+ $self->users->remove_follower(
+ uid => $user->{id},
+ follower => $target->{id}
+ );
+ }
+ elsif ( $action eq 'cancel_follow_request' ) {
+ $self->users->cancel_follow_request(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $action eq 'accept_follow_request' ) {
+ $self->users->accept_follow_request(
+ uid => $user->{id},
+ applicant => $target->{id}
+ );
+ }
+ elsif ( $action eq 'reject_follow_request' ) {
+ $self->users->reject_follow_request(
+ uid => $user->{id},
+ applicant => $target->{id}
+ );
+ }
+ elsif ( $action eq 'block' ) {
+ $self->users->block(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+ elsif ( $action eq 'unblock' ) {
+ $self->users->unblock(
+ uid => $user->{id},
+ target => $target->{id}
+ );
+ }
+
+ if ( $redirect_to eq 'profile' ) {
+
+ # profile links do not perform bulk actions
+ $self->redirect_to( '/p/' . $target->{name} );
+ return;
+ }
+ }
+
+ $self->redirect_to($redirect_to);
+}
+
+sub profile {
+ my ($self) = @_;
+ my $user = $self->current_user;
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+ my $md = Text::Markdown->new;
+ my $bio = $self->param('bio');
+
+ if ( length($bio) > 2000 ) {
+ $bio = substr( $bio, 0, 2000 ) . '…';
+ }
+
+ my $profile = {
+ bio => {
+ markdown => $bio,
+ html => $md->markdown( xml_escape($bio) ),
+ },
+ metadata => [],
+ };
+ for my $i ( 0 .. 20 ) {
+ my $key = $self->param("key_$i");
+ my $value = $self->param("value_$i");
+ if ($key) {
+ if ( length($value) > 500 ) {
+ $value = substr( $value, 0, 500 ) . '…';
+ }
+ my $html_value
+ = ( $value
+ =~ s{ \[ ([^]]+) \]\( ([^)]+) \) }{'<a href="' . xml_escape($2) . '" rel="me">' . xml_escape($1) .'</a>' }egrx
+ );
+ $profile->{metadata}[$i] = {
+ key => $key,
+ value => {
+ markdown => $value,
+ html => $html_value,
+ },
+ };
+ }
+ else {
+ last;
+ }
+ }
+ $self->users->set_profile(
+ uid => $user->{id},
+ profile => $profile
+ );
+ $self->redirect_to( '/p/' . $user->{name} );
+ }
+
+ my $profile = $self->users->get_profile( uid => $user->{id} );
+ $self->param( bio => $profile->{bio}{markdown} );
+ for my $i ( 0 .. $#{ $profile->{metadata} } ) {
+ $self->param( "key_$i" => $profile->{metadata}[$i]{key} );
+ $self->param( "value_$i" => $profile->{metadata}[$i]{value}{markdown} );
+ }
+
+ $self->render( 'edit_profile', name => $user->{name} );
+}
+
sub insight {
my ($self) = @_;
my $user = $self->current_user;
- my $use_history = $self->account_use_history( $user->{id} );
+ my $use_history = $self->users->use_history( uid => $user->{id} );
if ( $self->param('action') and $self->param('action') eq 'save' ) {
if ( $self->param('on_departure') ) {
@@ -273,7 +817,10 @@ sub insight {
$use_history &= ~0x02;
}
- $self->account_use_history( $user->{id}, $use_history );
+ $self->users->use_history(
+ uid => $user->{id},
+ set => $use_history
+ );
$self->flash( success => 'use_history' );
$self->redirect_to('account');
}
@@ -284,16 +831,42 @@ sub insight {
}
+sub services {
+ my ($self) = @_;
+ my $user = $self->current_user;
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ my $sb = $self->param('stationboard');
+ my $value = 0;
+ if ( $sb =~ m{ ^ \d+ $ }x and $sb >= 0 and $sb <= 4 ) {
+ $value = int($sb);
+ }
+ $self->users->use_external_services(
+ uid => $user->{id},
+ set => $value
+ );
+ $self->flash( success => 'external' );
+ $self->redirect_to('account');
+ }
+
+ $self->param( stationboard =>
+ $self->users->use_external_services( uid => $user->{id} ) );
+ $self->render('use_external_links');
+}
+
sub webhook {
my ($self) = @_;
- my $hook = $self->get_webhook;
+ my $uid = $self->current_user->{id};
+
+ my $hook = $self->users->get_webhook( uid => $uid );
if ( $self->param('action') and $self->param('action') eq 'save' ) {
$hook->{url} = $self->param('url');
$hook->{token} = $self->param('token');
$hook->{enabled} = $self->param('enabled') // 0;
- $self->set_webhook(
+ $self->users->set_webhook(
+ uid => $uid,
url => $hook->{url},
token => $hook->{token},
enabled => $hook->{enabled}
@@ -304,7 +877,7 @@ sub webhook {
sub {
$self->render(
'webhooks',
- hook => $self->get_webhook,
+ hook => $self->users->get_webhook( uid => $uid ),
new_hook => 1
);
}
@@ -330,8 +903,9 @@ sub change_mail {
if ( $action and $action eq 'update_mail' ) {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'change_mail',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
@@ -353,41 +927,17 @@ sub change_mail {
}
my $token = make_token();
- my $name = $self->current_user->{name};
my $db = $self->pg->db;
my $tx = $db->begin;
- $self->mark_for_mail_change( $db, $self->current_user->{id},
- $email, $token );
-
- my $ip = $self->req->headers->header('X-Forwarded-For');
- my $ua = $self->req->headers->user_agent;
- my $date = DateTime->now( time_zone => 'Europe/Berlin' )
- ->strftime('%d.%m.%Y %H:%M:%S %z');
-
- # In case Mojolicious is not running behind a reverse proxy
- $ip
- //= sprintf( '%s:%s', $self->tx->remote_address,
- $self->tx->remote_port );
- my $confirm_url
- = $self->url_for('confirm_mail')->to_abs->scheme('https');
- my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
-
- my $body = "Hallo ${name},\n\n";
- $body .= "Bitte bestätige unter <${confirm_url}/${token}>,\n";
- $body .= "dass du mit dieser Adresse E-Mail empfangen kannst.\n\n";
- $body
- .= "Du erhältst diese Mail, da eine Änderung der deinem travelynx-Account\n";
- $body .= "zugeordneten Mail-Adresse beantragt wurde.\n\n";
- $body .= "Daten zur Anfrage:\n";
- $body .= " * Datum: ${date}\n";
- $body .= " * Client: ${ip}\n";
- $body .= " * UserAgent: ${ua}\n\n\n";
- $body .= "Impressum: ${imprint_url}\n";
-
- my $success
- = $self->sendmail->custom( $email,
- 'travelynx: Mail-Adresse bestätigen', $body );
+ $self->users->mark_for_mail_change(
+ db => $db,
+ uid => $self->current_user->{id},
+ email => $email,
+ token => $token
+ );
+
+ my $success = $self->send_address_confirmation_mail( $email, $token );
if ($success) {
$tx->commit;
@@ -402,6 +952,70 @@ sub change_mail {
}
}
+sub change_name {
+ my ($self) = @_;
+
+ my $action = $self->req->param('action');
+ my $password = $self->req->param('password');
+ my $old_name = $self->current_user->{name};
+ my $new_name = $self->req->param('name');
+
+ if ( $action and $action eq 'update_name' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+
+ if ( my $error = $self->users->is_name_invalid( name => $new_name ) ) {
+ $self->render(
+ 'change_name',
+ name => $old_name,
+ invalid => $error
+ );
+ return;
+ }
+
+ if ( not $self->authenticate( $old_name, $self->param('password') ) ) {
+ $self->render(
+ 'change_name',
+ name => $old_name,
+ invalid => 'password'
+ );
+ return;
+ }
+
+ # The users table has a unique constraint on the "name" column, so having
+ # two users with the same name is not possible. The race condition
+ # between the user_name_exists check in is_name_invalid and this
+ # change_name call is harmless.
+ my $success = $self->users->change_name(
+ uid => $self->current_user->{id},
+ name => $new_name
+ );
+
+ if ( not $success ) {
+ $self->render(
+ 'change_name',
+ name => $old_name,
+ invalid => 'user_collision'
+ );
+ return;
+ }
+
+ $self->flash( success => 'name' );
+ $self->redirect_to('account');
+
+ $self->send_name_notification_mail( $old_name, $new_name );
+ }
+ else {
+ $self->render( 'change_name', name => $old_name );
+ }
+}
+
sub password_form {
my ($self) = @_;
@@ -415,7 +1029,11 @@ sub change_password {
my $password2 = $self->req->param('newpw2');
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'change_password', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
@@ -440,34 +1058,14 @@ sub change_password {
return;
}
- my $pw_hash = hash_password($password);
- $self->set_user_password( $self->current_user->{id}, $pw_hash );
+ $self->users->set_password(
+ uid => $self->current_user->{id},
+ password => $password
+ );
$self->flash( success => 'password' );
$self->redirect_to('account');
-
- my $user = $self->current_user->{name};
- my $email = $self->current_user->{email};
- my $ip = $self->req->headers->header('X-Forwarded-For');
- my $ua = $self->req->headers->user_agent;
- my $date = DateTime->now( time_zone => 'Europe/Berlin' )
- ->strftime('%d.%m.%Y %H:%M:%S %z');
-
- # In case Mojolicious is not running behind a reverse proxy
- $ip
- //= sprintf( '%s:%s', $self->tx->remote_address, $self->tx->remote_port );
- my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
-
- my $body = "Hallo ${user},\n\n";
- $body
- .= "Das Passwort deines travelynx-Accounts wurde soeben geändert.\n\n";
- $body .= "Daten zur Änderung:\n";
- $body .= " * Datum: ${date}\n";
- $body .= " * Client: ${ip}\n";
- $body .= " * UserAgent: ${ua}\n\n\n";
- $body .= "Impressum: ${imprint_url}\n";
-
- $self->sendmail->custom( $email, 'travelynx: Passwort geändert', $body );
+ $self->send_password_notification_mail();
}
sub request_password_reset {
@@ -475,14 +1073,21 @@ sub request_password_reset {
if ( $self->param('action') and $self->param('action') eq 'initiate' ) {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'recover_password', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
my $name = $self->param('user');
my $email = $self->param('email');
- my $uid = $self->get_uid_by_name_and_mail( $name, $email );
+ my $uid = $self->users->get_uid_by_name_and_mail(
+ name => $name,
+ email => $email
+ );
if ( not $uid ) {
$self->render( 'recover_password',
@@ -494,43 +1099,23 @@ sub request_password_reset {
my $db = $self->pg->db;
my $tx = $db->begin;
- my $error = $self->mark_for_password_reset( $db, $uid, $token );
+ my $error = $self->users->mark_for_password_reset(
+ db => $db,
+ uid => $uid,
+ token => $token
+ );
if ($error) {
$self->render( 'recover_password', invalid => $error );
return;
}
- my $ip = $self->req->headers->header('X-Forwarded-For');
- my $ua = $self->req->headers->user_agent;
- my $date = DateTime->now( time_zone => 'Europe/Berlin' )
- ->strftime('%d.%m.%Y %H:%M:%S %z');
-
- # In case Mojolicious is not running behind a reverse proxy
- $ip
- //= sprintf( '%s:%s', $self->tx->remote_address,
- $self->tx->remote_port );
- my $recover_url = $self->url_for('recover')->to_abs->scheme('https');
- my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
-
- my $body = "Hallo ${name},\n\n";
- $body .= "Unter ${recover_url}/${uid}/${token}\n";
- $body
- .= "kannst du ein neues Passwort für deinen travelynx-Account vergeben.\n\n";
- $body
- .= "Du erhältst diese Mail, da mit deinem Accountnamen und deiner Mail-Adresse\n";
- $body
- .= "ein Passwort-Reset angefordert wurde. Falls diese Anfrage nicht von dir\n";
- $body .= "ausging, kannst du sie ignorieren.\n\n";
- $body .= "Daten zur Anfrage:\n";
- $body .= " * Datum: ${date}\n";
- $body .= " * Client: ${ip}\n";
- $body .= " * UserAgent: ${ua}\n\n\n";
- $body .= "Impressum: ${imprint_url}\n";
-
- my $success
- = $self->sendmail->custom( $email, 'travelynx: Neues Passwort',
- $body );
+ my $success = $self->send_lostpassword_confirmation_mail(
+ email => $email,
+ name => $name,
+ uid => $uid,
+ token => $token
+ );
if ($success) {
$tx->commit;
@@ -549,10 +1134,20 @@ sub request_password_reset {
my $password2 = $self->param('newpw2');
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'set_password', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
- if ( not $self->verify_password_token( $id, $token ) ) {
+ if (
+ not $self->users->verify_password_token(
+ uid => $id,
+ token => $token
+ )
+ )
+ {
$self->render( 'recover_password', invalid => 'change token' );
return;
}
@@ -566,8 +1161,10 @@ sub request_password_reset {
return;
}
- my $pw_hash = hash_password($password);
- $self->set_user_password( $id, $pw_hash );
+ $self->users->set_password(
+ uid => $id,
+ password => $password
+ );
my $account = $self->get_user_data($id);
@@ -579,33 +1176,12 @@ sub request_password_reset {
$self->flash( success => 'password' );
$self->redirect_to('account');
- $self->remove_password_token( $id, $token );
-
- my $user = $account->{name};
- my $email = $account->{email};
- my $ip = $self->req->headers->header('X-Forwarded-For');
- my $ua = $self->req->headers->user_agent;
- my $date = DateTime->now( time_zone => 'Europe/Berlin' )
- ->strftime('%d.%m.%Y %H:%M:%S %z');
-
- # In case Mojolicious is not running behind a reverse proxy
- $ip
- //= sprintf( '%s:%s', $self->tx->remote_address,
- $self->tx->remote_port );
- my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
-
- my $body = "Hallo ${user},\n\n";
- $body
- .= "Das Passwort deines travelynx-Accounts wurde soeben über die";
- $body .= " 'Passwort vergessen'-Funktion geändert.\n\n";
- $body .= "Daten zur Änderung:\n";
- $body .= " * Datum: ${date}\n";
- $body .= " * Client: ${ip}\n";
- $body .= " * UserAgent: ${ua}\n\n\n";
- $body .= "Impressum: ${imprint_url}\n";
-
- $self->sendmail->custom( $email, 'travelynx: Passwort geändert',
- $body );
+ $self->users->remove_password_token(
+ uid => $id,
+ token => $token
+ );
+
+ $self->send_lostpassword_notification_mail($account);
}
else {
$self->render('recover_password');
@@ -623,7 +1199,13 @@ sub recover_password {
return;
}
- if ( $self->verify_password_token( $id, $token ) ) {
+ if (
+ $self->users->verify_password_token(
+ uid => $id,
+ token => $token
+ )
+ )
+ {
$self->render('set_password');
}
else {
@@ -636,7 +1218,22 @@ sub confirm_mail {
my $id = $self->current_user->{id};
my $token = $self->stash('token');
- if ( $self->change_mail_with_token( $id, $token ) ) {
+ # Some mail clients include the trailing ">" from the confirmation mail
+ # when opening/copying the confirmation link. A token will never contain
+ # this symbol, so remove it just in case.
+ $token =~ s{>}{};
+
+ # I did not yet find a mail client that also includes the trailing ",",
+ # but you never now...
+ $token =~ s{,}{};
+
+ if (
+ $self->users->change_mail_with_token(
+ uid => $id,
+ token => $token
+ )
+ )
+ {
$self->flash( success => 'mail' );
$self->redirect_to('account');
}
@@ -646,10 +1243,27 @@ sub confirm_mail {
}
sub account {
- my ($self) = @_;
+ my ($self) = @_;
+ my $uid = $self->current_user->{id};
+ my $rx_follow_requests = $self->users->has_follow_requests( uid => $uid );
+ my $tx_follow_requests = $self->users->has_follow_requests(
+ uid => $uid,
+ sent => 1
+ );
+ my $followers = $self->users->has_followers( uid => $uid );
+ my $following = $self->users->has_followees( uid => $uid );
+ my $blocked = $self->users->has_blocked_users( uid => $uid );
- $self->render('account');
- $self->mark_seen( $self->current_user->{id} );
+ $self->render(
+ 'account',
+ api_token => $self->users->get_api_token( uid => $uid ),
+ num_rx_follow_requests => $rx_follow_requests,
+ num_tx_follow_requests => $tx_follow_requests,
+ num_followers => $followers,
+ num_following => $following,
+ num_blocked => $blocked,
+ );
+ $self->users->mark_seen( uid => $uid );
}
sub json_export {
@@ -660,7 +1274,7 @@ sub json_export {
$self->render(
json => {
- account => $db->select( 'users', '*', { id => $uid } )->hash,
+ account => $db->select( 'users', '*', { id => $uid } )->hash,
in_transit => [
$db->select( 'in_transit_str', '*', { user_id => $uid } )
->hashes->each
@@ -673,4 +1287,53 @@ sub json_export {
);
}
+sub webfinger {
+ my ($self) = @_;
+
+ my $resource = $self->param('resource');
+
+ if ( not $resource ) {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ my $root_url = $self->base_url_for('/')->to_abs->host;
+
+ if ( not $root_url
+ or not $resource
+ =~ m{ ^ acct: [@]? (?<name> [^@]+ ) [@] $root_url $ }x )
+ {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ my $name = $+{name};
+ my $user = $self->users->get_privacy_by( name => $name );
+
+ if ( not $user ) {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ my $profile_url
+ = $self->base_url_for("/p/${name}")->to_abs->scheme('https')->to_string;
+
+ $self->render(
+ text => JSON->new->encode(
+ {
+ subject => $resource,
+ aliases => [ $profile_url, ],
+ links => [
+ {
+ rel => 'http://webfinger.net/rel/profile-page',
+ type => 'text/html',
+ href => $profile_url,
+ },
+ ],
+ }
+ ),
+ format => 'json',
+ );
+}
+
1;
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index 4546292..687243d 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -1,11 +1,17 @@
package Travelynx::Controller::Api;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use List::Util;
-use Travel::Status::DE::IRIS::Stations;
+use Mojo::JSON qw(encode_json);
use UUID::Tiny qw(:std);
+# Internal Helpers
+
sub make_token {
return create_uuid_as_string(UUID_V4);
}
@@ -18,13 +24,28 @@ sub sanitize {
if ( $type eq '' ) {
return '' . $value;
}
- return 0 + $value;
+ if ( $value =~ m{ ^ [0-9.e]+ $ }x ) {
+ return 0 + $value;
+ }
+ return 0;
}
+# Contollers
+
sub documentation {
my ($self) = @_;
- $self->render('api_documentation');
+ if ( $self->is_user_authenticated ) {
+ my $uid = $self->current_user->{id};
+ $self->render(
+ 'api_documentation',
+ uid => $uid,
+ api_token => $self->users->get_api_token( uid => $uid ),
+ );
+ }
+ else {
+ $self->render('api_documentation');
+ }
}
sub get_v1 {
@@ -60,8 +81,11 @@ sub get_v1 {
return;
}
- my $token = $self->get_api_token($uid);
- if ( $api_token ne $token->{$api_action} ) {
+ my $token = $self->users->get_api_token( uid => $uid );
+ if ( not $api_token
+ or not $token->{$api_action}
+ or $api_token ne $token->{$api_action} )
+ {
$self->render(
json => {
error => 'Invalid token',
@@ -70,7 +94,7 @@ sub get_v1 {
return;
}
if ( $api_action eq 'status' ) {
- $self->render( json => $self->get_user_status_json_v1($uid) );
+ $self->render( json => $self->get_user_status_json_v1( uid => $uid ) );
}
else {
$self->render(
@@ -97,18 +121,6 @@ sub travel_v1 {
return;
}
- if ( $self->app->mode ne 'development' ) {
- $self->render(
- json => {
- success => \0,
- deprecated => \0,
- error =>
-'This feature is incomplete and only available in development mode',
- },
- );
- return;
- }
-
my $api_token = $payload->{token} // '';
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
@@ -135,7 +147,7 @@ sub travel_v1 {
return;
}
- my $token = $self->get_api_token($uid);
+ my $token = $self->users->get_api_token( uid => $uid );
if ( not $token->{'travel'} or $api_token ne $token->{'travel'} ) {
$self->render(
json => {
@@ -155,7 +167,7 @@ sub travel_v1 {
success => \0,
deprecated => \0,
error => 'Missing or invalid action',
- status => $self->get_user_status_json_v1($uid)
+ status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
@@ -165,12 +177,14 @@ sub travel_v1 {
my $from_station = sanitize( q{}, $payload->{fromStation} );
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
+ my $hafas = exists $payload->{train}{journeyID} ? 1 : 0;
if (
not(
$from_station
- and ( ( $payload->{train}{type} and $payload->{train}{no} )
- or $payload->{train}{id} )
+ and ( ( $payload->{train}{type} and $payload->{train}{no} )
+ or $payload->{train}{id}
+ or $payload->{train}{journeyID} )
)
)
{
@@ -179,77 +193,139 @@ sub travel_v1 {
success => \0,
deprecated => \0,
error => 'Missing fromStation or train data',
- status => $self->get_user_status_json_v1($uid)
+ status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
- if ( exists $payload->{train}{id} ) {
- $train_id = sanitize( 0, $payload->{train}{id} );
+ if ( not $hafas and not $self->stations->search($from_station) ) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Unknown fromStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ if ( $to_station
+ and not $hafas
+ and not $self->stations->search($to_station) )
+ {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Unknown toStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ my $train_p;
+
+ if ( exists $payload->{train}{journeyID} ) {
+ $train_p = Mojo::Promise->resolve(
+ sanitize( q{}, $payload->{train}{journeyID} ) );
+ }
+ elsif ( exists $payload->{train}{id} ) {
+ $train_p
+ = Mojo::Promise->resolve( sanitize( 0, $payload->{train}{id} ) );
}
else {
my $train_type = sanitize( q{}, $payload->{train}{type} );
my $train_no = sanitize( q{}, $payload->{train}{no} );
- my $status = $self->get_departures( $from_station, 140, 40, 0 );
- if ( $status->{errstr} ) {
+
+ $train_p = $self->iris->get_departures_p(
+ station => $from_station,
+ lookbehind => 140,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($status) = @_;
+ if ( $status->{errstr} ) {
+ return Mojo::Promise->reject(
+ 'Error requesting departures from fromStation: '
+ . $status->{errstr} );
+ }
+ my ($train) = List::Util::first {
+ $_->type eq $train_type and $_->train_no eq $train_no
+ }
+ @{ $status->{results} };
+ if ( not defined $train ) {
+ return Mojo::Promise->reject(
+ 'Train not found at fromStation');
+ }
+ return Mojo::Promise->resolve( $train->train_id );
+ }
+ );
+ }
+
+ $self->render_later;
+
+ $train_p->then(
+ sub {
+ my ($train_id) = @_;
+ return $self->checkin_p(
+ station => $from_station,
+ train_id => $train_id,
+ uid => $uid
+ );
+ }
+ )->then(
+ sub {
+ my ($train) = @_;
+ if ( $payload->{comment} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ user_data =>
+ { comment => sanitize( q{}, $payload->{comment} ) }
+ );
+ }
+ if ($to_station) {
+
+ # the user may not have provided the correct to_station, so
+ # request related stations for checkout.
+ return $self->checkout_p(
+ station => $to_station,
+ force => 0,
+ uid => $uid,
+ with_related => 1,
+ );
+ }
+ return Mojo::Promise->resolve;
+ }
+ )->then(
+ sub {
+ my ( undef, $error ) = @_;
+ if ($error) {
+ return Mojo::Promise->reject($error);
+ }
$self->render(
json => {
- success => \0,
- error =>
- 'Error requesting departures from fromStation: '
- . $status->{errstr},
- status => $self->get_user_status_json_v1($uid)
+ success => \1,
+ deprecated => \0,
+ status => $self->get_user_status_json_v1( uid => $uid )
}
);
- return;
- }
- my ($train) = List::Util::first {
- $_->type eq $train_type and $_->train_no eq $train_no
}
- @{ $status->{results} };
- if ( not defined $train ) {
+ )->catch(
+ sub {
+ my ($error) = @_;
$self->render(
json => {
success => \0,
deprecated => \0,
- error => 'Train not found at fromStation',
- status => $self->get_user_status_json_v1($uid)
+ error => 'Checkin/Checkout error: ' . $error,
+ status => $self->get_user_status_json_v1( uid => $uid )
}
);
- return;
}
- $train_id = $train->train_id;
- }
-
- my ( $train, $error )
- = $self->checkin( $from_station, $train_id, $uid );
- if ( $payload->{comment} and not $error ) {
- $self->update_in_transit_comment(
- sanitize( q{}, $payload->{comment} ), $uid );
- }
- if ( $to_station and not $error ) {
- ( $train, $error ) = $self->checkout( $to_station, 0, $uid );
- }
- if ($error) {
- $self->render(
- json => {
- success => \0,
- deprecated => \0,
- error => 'Checkin/Checkout error: ' . $error,
- status => $self->get_user_status_json_v1($uid)
- }
- );
- }
- else {
- $self->render(
- json => {
- success => \1,
- deprecated => \0,
- status => $self->get_user_status_json_v1($uid)
- }
- );
- }
+ )->wait;
}
elsif ( $payload->{action} eq 'checkout' ) {
my $to_station = sanitize( q{}, $payload->{toStation} );
@@ -260,38 +336,56 @@ sub travel_v1 {
success => \0,
deprecated => \0,
error => 'Missing toStation',
- status => $self->get_user_status_json_v1($uid)
+ status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
if ( $payload->{comment} ) {
- $self->update_in_transit_comment(
- sanitize( q{}, $payload->{comment} ), $uid );
- }
-
- my ( $train, $error )
- = $self->checkout( $to_station, $payload->{force} ? 1 : 0, $uid );
- if ($error) {
- $self->render(
- json => {
- success => \0,
- deprecated => \0,
- error => 'Checkout error: ' . $error,
- status => $self->get_user_status_json_v1($uid)
- }
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ user_data => { comment => sanitize( q{}, $payload->{comment} ) }
);
}
- else {
- $self->render(
- json => {
- success => \1,
- deprecated => \0,
- status => $self->get_user_status_json_v1($uid)
+
+ $self->render_later;
+
+ # the user may not have provided the correct to_station, so
+ # request related stations for checkout.
+ $self->checkout_p(
+ station => $to_station,
+ force => $payload->{force} ? 1 : 0,
+ uid => $uid,
+ with_related => 1,
+ )->then(
+ sub {
+ my ( $train, $error ) = @_;
+ if ($error) {
+ return Mojo::Promise->reject($error);
}
- );
- }
+ $self->render(
+ json => {
+ success => \1,
+ deprecated => \0,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Checkout error: ' . $err,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
+ )->wait;
}
elsif ( $payload->{action} eq 'undo' ) {
my $error = $self->undo( 'in_transit', $uid );
@@ -301,7 +395,7 @@ sub travel_v1 {
success => \0,
deprecated => \0,
error => $error,
- status => $self->get_user_status_json_v1($uid)
+ status => $self->get_user_status_json_v1( uid => $uid )
}
);
}
@@ -310,7 +404,7 @@ sub travel_v1 {
json => {
success => \1,
deprecated => \0,
- status => $self->get_user_status_json_v1($uid)
+ status => $self->get_user_status_json_v1( uid => $uid )
}
);
}
@@ -325,19 +419,9 @@ sub import_v1 {
if ( not $payload or ref($payload) ne 'HASH' ) {
$self->render(
json => {
- success => \0,
- error => 'Malformed JSON',
- },
- );
- return;
- }
-
- if ( $self->app->mode ne 'development' ) {
- $self->render(
- json => {
- success => \0,
- error =>
-'This feature is incomplete and only available in development mode',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed JSON',
},
);
return;
@@ -348,8 +432,9 @@ sub import_v1 {
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
- success => \0,
- error => 'Malformed token',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
},
);
return;
@@ -360,19 +445,21 @@ sub import_v1 {
if ( $uid > 2147483647 ) {
$self->render(
json => {
- success => \0,
- error => 'Malformed token',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
},
);
return;
}
- my $token = $self->get_api_token($uid);
- if ( $api_token ne $token->{'import'} ) {
+ my $token = $self->users->get_api_token( uid => $uid );
+ if ( not $token->{'import'} or $api_token ne $token->{'import'} ) {
$self->render(
json => {
- success => \0,
- error => 'Invalid token',
+ success => \0,
+ deprecated => \0,
+ error => 'Invalid token',
},
);
return;
@@ -383,8 +470,9 @@ sub import_v1 {
{
$self->render(
json => {
- success => \0,
- error => 'missing fromStation or toStation',
+ success => \0,
+ deprecated => \0,
+ error => 'missing fromStation or toStation',
},
);
return;
@@ -409,13 +497,13 @@ sub import_v1 {
}
%opt = (
- uid => $uid,
- train_type => sanitize( q{}, $payload->{train}{type} ),
- train_no => sanitize( q{}, $payload->{train}{no} ),
- train_line => sanitize( q{}, $payload->{train}{line} ),
- cancelled => $payload->{cancelled} ? 1 : 0,
- dep_station => sanitize( q{}, $payload->{fromStation}{name} ),
- arr_station => sanitize( q{}, $payload->{toStation}{name} ),
+ uid => $uid,
+ train_type => sanitize( q{}, $payload->{train}{type} ),
+ train_no => sanitize( q{}, $payload->{train}{no} ),
+ train_line => sanitize( q{}, $payload->{train}{line} ),
+ cancelled => $payload->{cancelled} ? 1 : 0,
+ dep_station => sanitize( q{}, $payload->{fromStation}{name} ),
+ arr_station => sanitize( q{}, $payload->{toStation}{name} ),
sched_departure =>
sanitize( 0, $payload->{fromStation}{scheduledTime} ),
rt_departure => sanitize(
@@ -434,9 +522,12 @@ sub import_v1 {
lax => $payload->{lax} ? 1 : 0,
);
- if ( $payload->{route} and ref( $payload->{route} ) eq 'ARRAY' ) {
+ if ( $payload->{intermediateStops}
+ and ref( $payload->{intermediateStops} ) eq 'ARRAY' )
+ {
$opt{route}
- = [ map { sanitize( q{}, $_ ) } @{ $payload->{route} } ];
+ = [ map { sanitize( q{}, $_ ) }
+ @{ $payload->{intermediateStops} } ];
}
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
@@ -451,8 +542,9 @@ sub import_v1 {
my ($first_line) = split( qr{\n}, $@ );
$self->render(
json => {
- success => \0,
- error => $first_line
+ success => \0,
+ deprecated => \0,
+ error => $first_line
}
);
return;
@@ -462,44 +554,52 @@ sub import_v1 {
my $tx = $db->begin;
$opt{db} = $db;
- my ( $journey_id, $error ) = $self->add_journey(%opt);
+ my ( $journey_id, $error ) = $self->journeys->add(%opt);
my $journey;
if ( not $error ) {
- $journey = $self->get_journey(
+ $journey = $self->journeys->get_single(
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1
);
$error
- = $self->journey_sanity_check( $journey, $payload->{lax} ? 1 : 0 );
+ = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 );
}
if ($error) {
$self->render(
json => {
- success => \0,
- error => $error
+ success => \0,
+ deprecated => \0,
+ error => $error
}
);
}
elsif ( $payload->{dryRun} ) {
$self->render(
json => {
- success => \1,
- id => $journey_id,
- result => $journey
+ success => \1,
+ deprecated => \0,
+ id => $journey_id,
+ result => $journey
}
);
}
else {
+ $self->journey_stats_cache->invalidate(
+ ts => $opt{rt_departure},
+ db => $db,
+ uid => $uid
+ );
$tx->commit;
$self->render(
json => {
- success => \1,
- id => $journey_id,
- result => $journey
+ success => \1,
+ deprecated => \0,
+ id => $journey_id,
+ result => $journey
}
);
}
@@ -508,11 +608,15 @@ sub import_v1 {
sub set_token {
my ($self) = @_;
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'account', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
my $token = make_token();
- my $token_id = $self->app->token_type->{ $self->param('token') };
+ my $token_id = $self->users->get_token_id( $self->param('token') );
if ( not $token_id ) {
$self->redirect_to('account');
@@ -545,4 +649,21 @@ sub set_token {
$self->redirect_to('account');
}
+sub autocomplete {
+ my ($self) = @_;
+
+ $self->res->headers->cache_control('max-age=86400, immutable');
+
+ my $output
+ = "document.addEventListener('DOMContentLoaded',function(){M.Autocomplete.init(document.querySelectorAll('.autocomplete'),{\n";
+ $output .= 'minLength:3,limit:50,data:';
+ $output .= encode_json( $self->stations->get_for_autocomplete );
+ $output .= "\n});});\n";
+
+ $self->render(
+ format => 'js',
+ data => $output
+ );
+}
+
1;
diff --git a/lib/Travelynx/Controller/Passengerrights.pm b/lib/Travelynx/Controller/Passengerrights.pm
index 7d9a00b..d80f1ae 100644
--- a/lib/Travelynx/Controller/Passengerrights.pm
+++ b/lib/Travelynx/Controller/Passengerrights.pm
@@ -1,15 +1,22 @@
package Travelynx::Controller::Passengerrights;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use CAM::PDF;
+# Internal Helpers
+
sub mark_if_missed_connection {
my ( $self, $journey, $next_journey ) = @_;
my $possible_delay
= ( $next_journey->{rt_departure}->epoch
- - $journey->{sched_arrival}->epoch ) / 60;
+ - $journey->{sched_arrival}->epoch )
+ / 60;
my $wait_time
= ( $next_journey->{rt_departure}->epoch - $journey->{rt_arrival}->epoch )
/ 60;
@@ -50,7 +57,8 @@ sub mark_if_missed_connection {
sub mark_substitute_connection {
my ( $self, $journey ) = @_;
- my @substitute_candidates = reverse $self->get_user_travels(
+ my @substitute_candidates = reverse $self->journeys->get(
+ uid => $self->current_user->{id},
after => $journey->{sched_departure}->clone->subtract( hours => 1 ),
before => $journey->{sched_departure}->clone->add( hours => 12 ),
with_datetime => 1,
@@ -81,13 +89,16 @@ sub mark_substitute_connection {
}
}
+# Controllers
+
sub list_candidates {
my ($self) = @_;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $range_start = $now->clone->subtract( months => 6 );
- my @journeys = $self->get_user_travels(
+ my @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
after => $range_start,
before => $now,
with_datetime => 1,
@@ -112,7 +123,8 @@ sub list_candidates {
@journeys = grep { $_->{delay} >= 60 or $_->{connection_missed} } @journeys;
- my @cancelled = $self->get_user_travels(
+ my @cancelled = $self->journeys->get(
+ uid => $self->current_user->{id},
after => $range_start,
before => $now,
cancelled => 1,
@@ -163,7 +175,7 @@ sub generate {
return;
}
- my $journey = $self->get_journey(
+ my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
verbose => 1,
@@ -187,7 +199,7 @@ sub generate {
$self->mark_substitute_connection($journey);
}
elsif ( $journey->{delay} < 120 ) {
- my @connections = $self->get_user_travels(
+ my @connections = $self->journeys->get(
uid => $uid,
after => $journey->{rt_arrival},
before => $journey->{rt_arrival}->clone->add( hours => 2 ),
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
new file mode 100755
index 0000000..fc2d38c
--- /dev/null
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -0,0 +1,603 @@
+package Travelynx::Controller::Profile;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Controller';
+
+use DateTime;
+
+# Internal Helpers
+
+sub status_token_ok {
+ my ( $self, $status, $ts2_ext ) = @_;
+ my $token = $self->param('token') // q{};
+
+ my ( $eva, $ts, $ts2 ) = split( qr{-}, $token );
+ if ( not $ts ) {
+ return;
+ }
+
+ $ts2 //= $ts2_ext;
+
+ if ( $eva == $status->{dep_eva}
+ and $ts == $status->{timestamp}->epoch % 337
+ and $ts2 == $status->{sched_departure}->epoch )
+ {
+ return 1;
+ }
+ return;
+}
+
+sub journey_token_ok {
+ my ( $self, $journey, $ts2_ext ) = @_;
+ my $token = $self->param('token') // q{};
+
+ my ( $eva, $ts, $ts2 ) = split( qr{-}, $token );
+ if ( not $ts ) {
+ return;
+ }
+
+ $ts2 //= $ts2_ext;
+
+ if ( $eva == $journey->{from_eva}
+ and $ts == $journey->{checkin_ts} % 337
+ and $ts2 == $journey->{sched_dep_ts} )
+ {
+ return 1;
+ }
+ return;
+}
+
+# Controllers
+
+sub profile {
+ my ($self) = @_;
+
+ my $name = $self->stash('name');
+ my $user = $self->users->get_privacy_by( name => $name );
+
+ if ( not $user ) {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ my $profile = $self->users->get_profile( uid => $user->{id} );
+
+ my $my_user;
+ my $relation;
+ my $inverse_relation;
+ my $is_self;
+ if ( $self->is_user_authenticated ) {
+ $my_user = $self->current_user;
+ if ( $my_user->{id} == $user->{id} ) {
+ $is_self = 1;
+ $my_user = undef;
+ }
+ else {
+ $relation = $self->users->get_relation(
+ subject => $my_user->{id},
+ object => $user->{id}
+ );
+ $inverse_relation = $self->users->get_relation(
+ subject => $user->{id},
+ object => $my_user->{id}
+ );
+ }
+ }
+
+ my $status = $self->get_user_status( $user->{id} );
+ if ( $status->{checked_in} or $status->{arr_name} ) {
+ my $visibility = $status->{effective_visibility};
+ if (
+ not(
+ $visibility == 100
+ or ( $visibility >= 80 and $my_user )
+ or
+ ( $visibility >= 60 and $relation and $relation eq 'follows' )
+ or ( $visibility >= 60 and $is_self )
+ or ( $visibility >= 30 and $self->status_token_ok($status) )
+ )
+ )
+ {
+ $status->{checked_in} = 0;
+ $status->{arr_name} = undef;
+ }
+ }
+ if ( not $status->{checked_in}
+ and $status->{arr_name}
+ and not $user->{past_status} )
+ {
+ $status->{arr_name} = undef;
+ }
+
+ my @journeys;
+
+ if (
+ $user->{past_visibility_str} eq 'public'
+ or ( $user->{past_visibility_str} eq 'travelynx'
+ and ( $my_user or $is_self ) )
+ or ( $user->{past_visibility_str} eq 'followers'
+ and ( ( $relation and $relation eq 'follows' ) or $is_self ) )
+ )
+ {
+
+ my %opt = (
+ uid => $user->{id},
+ limit => 10,
+ with_datetime => 1
+ );
+
+ if ( not $user->{past_all} ) {
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{before} = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{after} = $now->clone->subtract( weeks => 4 );
+ }
+
+ if ($is_self) {
+ $opt{min_visibility} = 'followers';
+ }
+ elsif ($my_user) {
+ if ( $relation and $relation eq 'follows' ) {
+ $opt{min_visibility} = 'followers';
+ }
+ else {
+ $opt{min_visibility} = 'travelynx';
+ }
+ }
+ else {
+ $opt{min_visibility} = 'public';
+ }
+
+ @journeys = $self->journeys->get(%opt);
+ }
+
+ $self->render(
+ 'profile',
+ title => "travelynx: $name",
+ name => $name,
+ uid => $user->{id},
+ privacy => $user,
+ bio => $profile->{bio}{html},
+ metadata => $profile->{metadata},
+ is_self => $is_self,
+ following => ( $relation and $relation eq 'follows' ) ? 1 : 0,
+ follow_requested => ( $relation and $relation eq 'requests_follow' )
+ ? 1
+ : 0,
+ can_follow => ( $my_user and $user->{accept_follows} and not $relation )
+ ? 1
+ : 0,
+ can_request_follow =>
+ ( $my_user and $user->{accept_follow_requests} and not $relation )
+ ? 1
+ : 0,
+ follows_me => ( $inverse_relation and $inverse_relation eq 'follows' )
+ ? 1
+ : 0,
+ follow_reqs_me =>
+ ( $inverse_relation and $inverse_relation eq 'requests_follow' ) ? 1
+ : 0,
+ journey => $status,
+ journeys => [@journeys],
+ );
+}
+
+sub journey_details {
+ my ($self) = @_;
+ my $name = $self->stash('name');
+ my $journey_id = $self->stash('id');
+ my $user = $self->users->get_privacy_by( name => $name );
+
+ $self->param( journey_id => $journey_id );
+
+ my $my_user;
+ my $relation;
+ my $inverse_relation;
+ my $is_self;
+ if ( $self->is_user_authenticated ) {
+ $my_user = $self->current_user;
+ if ( $my_user->{id} == $user->{id} ) {
+ $is_self = 1;
+ $my_user = undef;
+ }
+ else {
+ $relation = $self->users->get_relation(
+ subject => $my_user->{id},
+ object => $user->{id}
+ );
+ }
+ }
+
+ if ( not( $user and $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
+ $self->render(
+ 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ );
+ return;
+ }
+
+ my $journey = $self->journeys->get_single(
+ uid => $user->{id},
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
+ );
+
+ if ( not $journey ) {
+ $self->render(
+ 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ );
+ return;
+ }
+
+ my $is_past;
+ if ( not $user->{past_all} ) {
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ if ( $journey->{sched_dep_ts} < $now->subtract( weeks => 4 )->epoch ) {
+ $is_past = 1;
+ }
+ }
+
+ my $visibility = $journey->{effective_visibility};
+
+ if (
+ not( ( $visibility == 100 and not $is_past )
+ or ( $visibility >= 80 and $my_user and not $is_past )
+ or ( $visibility >= 60 and $relation and $relation eq 'follows' )
+ or ( $visibility >= 60 and $is_self )
+ or ( $visibility >= 30 and $self->journey_token_ok($journey) ) )
+ )
+ {
+ $self->render(
+ 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ );
+ return;
+ }
+
+ my $title = sprintf( 'Fahrt von %s nach %s am %s',
+ $journey->{from_name}, $journey->{to_name},
+ $journey->{rt_arrival}->strftime('%d.%m.%Y') );
+ my $delay = 'pünktlich ';
+ if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) {
+ $delay = sprintf(
+ 'mit %+d ',
+ (
+ $journey->{rt_arrival}->epoch
+ - $journey->{sched_arrival}->epoch
+ ) / 60
+ );
+ }
+ my $description = sprintf( 'Ankunft mit %s %s %s',
+ $journey->{type}, $journey->{no},
+ $journey->{rt_arrival}->strftime('um %H:%M') );
+ if ( $journey->{km_route} > 0.1 ) {
+ $description = sprintf( '%.0f km mit %s %s – Ankunft %sum %s',
+ $journey->{km_route}, $journey->{type}, $journey->{no},
+ $delay, $journey->{rt_arrival}->strftime('%H:%M') );
+ }
+ my %tw_data = (
+ card => 'summary',
+ site => '@derfnull',
+ image => $self->url_for('/static/icons/icon-512x512.png')
+ ->to_abs->scheme('https'),
+ title => $title,
+ description => $description,
+ );
+ my %og_data = (
+ type => 'article',
+ image => $tw_data{image},
+ url => $self->url_for->to_abs,
+ site_name => 'travelynx',
+ title => $title,
+ description => $description,
+ );
+
+ my $map_data = $self->journeys_to_map_data(
+ journeys => [$journey],
+ include_manual => 1,
+ );
+ if ( $journey->{user_data}{comment}
+ and not $user->{comments_visible} )
+ {
+ delete $journey->{user_data}{comment};
+ }
+ $self->render(
+ 'journey',
+ title => "travelynx: $title",
+ error => undef,
+ journey => $journey,
+ with_map => 1,
+ username => $name,
+ readonly => 1,
+ twitter => \%tw_data,
+ opengraph => \%og_data,
+ %{$map_data},
+ );
+}
+
+sub user_status {
+ my ($self) = @_;
+
+ my $name = $self->stash('name');
+ my $ts = $self->stash('ts') // 0;
+ my $user = $self->users->get_privacy_by( name => $name );
+
+ if ( not $user ) {
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
+ return;
+ }
+
+ my $my_user;
+ my $relation;
+ my $inverse_relation;
+ my $is_self;
+ if ( $self->is_user_authenticated ) {
+ $my_user = $self->current_user;
+ if ( $my_user->{id} == $user->{id} ) {
+ $is_self = 1;
+ $my_user = undef;
+ }
+ else {
+ $relation = $self->users->get_relation(
+ subject => $my_user->{id},
+ object => $user->{id}
+ );
+ }
+ }
+
+ my $status = $self->get_user_status( $user->{id} );
+
+ if (
+ $ts
+ and ( not $status->{checked_in}
+ or $status->{sched_departure}->epoch != $ts )
+ )
+ {
+ for my $journey (
+ $self->journeys->get(
+ uid => $user->{id},
+ sched_dep_ts => $ts,
+ limit => 1,
+ with_visibility => 1,
+ )
+ )
+ {
+ my $visibility = $journey->{effective_visibility};
+ if (
+ $visibility == 100
+ or ( $visibility >= 80 and $my_user )
+ or
+ ( $visibility >= 60 and $relation and $relation eq 'follows' )
+ or ( $visibility >= 60 and $is_self )
+ or ( $visibility >= 30
+ and $self->journey_token_ok( $journey, $ts ) )
+ )
+ {
+ my $token = $self->param('token') // q{};
+ $self->redirect_to(
+ "/p/${name}/j/$journey->{id}?token=${token}-${ts}");
+ }
+ else {
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
+ }
+ return;
+ }
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
+ return;
+ }
+
+ my %tw_data = (
+ card => 'summary',
+ site => '@derfnull',
+ image => $self->url_for('/static/icons/icon-512x512.png')
+ ->to_abs->scheme('https'),
+ );
+ my %og_data = (
+ type => 'article',
+ image => $tw_data{image},
+ url => $self->url_for("/status/${name}")->to_abs->scheme('https'),
+ site_name => 'travelynx',
+ );
+
+ if ( $status->{checked_in} or $status->{arr_name} ) {
+ my $visibility = $status->{effective_visibility};
+ if (
+ not(
+ $visibility == 100
+ or ( $visibility >= 80 and $my_user )
+ or
+ ( $visibility >= 60 and $relation and $relation eq 'follows' )
+ or ( $visibility >= 60 and $is_self )
+ or
+ ( $visibility >= 30 and $self->status_token_ok( $status, $ts ) )
+ )
+ )
+ {
+ $status = {};
+ }
+ }
+ if ( not $status->{checked_in}
+ and $status->{arr_name}
+ and not $user->{past_status} )
+ {
+ $status = {};
+ }
+
+ if ( $status->{checked_in} ) {
+ $og_data{url} .= '/' . $status->{sched_departure}->epoch;
+ $og_data{title} = $tw_data{title} = "${name} ist unterwegs";
+ $og_data{description} = $tw_data{description} = sprintf(
+ '%s %s von %s nach %s',
+ $status->{train_type}, $status->{train_line} // $status->{train_no},
+ $status->{dep_name}, $status->{arr_name} // 'irgendwo'
+ );
+ if ( $status->{real_arrival}->epoch ) {
+ $tw_data{description} .= $status->{real_arrival}
+ ->strftime(' – Ankunft gegen %H:%M Uhr');
+ $og_data{description} .= $status->{real_arrival}
+ ->strftime(' – Ankunft gegen %H:%M Uhr');
+ }
+ }
+ else {
+ $og_data{title} = $tw_data{title}
+ = "${name} ist gerade nicht eingecheckt";
+ $og_data{description} = $tw_data{description} = q{};
+ }
+
+ $self->respond_to(
+ json => {
+ json => {
+ account => {
+ name => $name,
+ },
+ status => $self->get_user_status_json_v1(
+ status => $status,
+ privacy => $user,
+ public => 1
+ ),
+ version => $self->app->config->{version} // 'UNKNOWN',
+ },
+ },
+ any => {
+ template => 'user_status',
+ name => $name,
+ title => "travelynx: $tw_data{title}",
+ privacy => $user,
+ journey => $status,
+ twitter => \%tw_data,
+ opengraph => \%og_data,
+ version => $self->app->config->{version} // 'UNKNOWN',
+ },
+ );
+}
+
+sub status_card {
+ my ($self) = @_;
+
+ my $name = $self->stash('name');
+ $name =~ s{[.]html$}{};
+ my $user = $self->users->get_privacy_by( name => $name );
+
+ delete $self->stash->{layout};
+
+ if ( not $user ) {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ my $my_user;
+ my $relation;
+ my $inverse_relation;
+ my $is_self;
+ if ( $self->is_user_authenticated ) {
+ $my_user = $self->current_user;
+ if ( $my_user->{id} == $user->{id} ) {
+ $is_self = 1;
+ $my_user = undef;
+ }
+ else {
+ $relation = $self->users->get_relation(
+ subject => $my_user->{id},
+ object => $user->{id}
+ );
+ }
+ }
+
+ my $status = $self->get_user_status( $user->{id} );
+ my $visibility;
+ if ( $status->{checked_in} or $status->{arr_name} ) {
+ my $visibility = $status->{effective_visibility};
+ if (
+ not(
+ $visibility == 100
+ or ( $visibility >= 80 and $my_user )
+ or
+ ( $visibility >= 60 and $relation and $relation eq 'follows' )
+ or ( $visibility >= 60 and $is_self )
+ or ( $visibility >= 30 and $self->status_token_ok($status) )
+ )
+ )
+ {
+ $status->{checked_in} = 0;
+ $status->{arr_name} = undef;
+ }
+ }
+ if ( not $status->{checked_in}
+ and $status->{arr_name}
+ and not $user->{past_status} )
+ {
+ $status->{arr_name} = undef;
+ }
+
+ $self->render(
+ '_public_status_card',
+ name => $name,
+ privacy => $user,
+ journey => $status,
+ from_profile => $self->param('profile') ? 1 : 0,
+ );
+}
+
+sub checked_in {
+ my ($self) = @_;
+
+ my $uid = $self->current_user->{id};
+ my @journeys = $self->in_transit->get_timeline(
+ uid => $uid,
+ with_data => 1
+ );
+
+ if ( $self->param('ajax') ) {
+ delete $self->stash->{layout};
+ $self->render(
+ '_timeline-checked-in',
+ journeys => [@journeys],
+ );
+ }
+ else {
+ $self->render(
+ 'timeline-checked-in',
+ journeys => [@journeys],
+ );
+ }
+}
+
+1;
diff --git a/lib/Travelynx/Controller/Static.pm b/lib/Travelynx/Controller/Static.pm
index 2c35f1b..04c2d0f 100644
--- a/lib/Travelynx/Controller/Static.pm
+++ b/lib/Travelynx/Controller/Static.pm
@@ -1,26 +1,32 @@
package Travelynx::Controller::Static;
-use Mojo::Base 'Mojolicious::Controller';
-my $travelynx_version = qx{git describe --dirty} || 'experimental';
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Controller';
sub about {
my ($self) = @_;
- $self->render( 'about',
- version => $self->app->config->{version} // 'UNKNOWN' );
+ $self->render( 'about', title => 'Über travelynx' );
}
sub changelog {
my ($self) = @_;
- $self->render( 'changelog',
- version => $self->app->config->{version} // 'UNKNOWN' );
+ $self->render( 'changelog', title => 'travelynx: Changelog' );
}
sub imprint {
my ($self) = @_;
- $self->render('imprint');
+ $self->render( 'imprint', title => 'travelynx: Impressum' );
+}
+
+sub legend {
+ my ($self) = @_;
+
+ $self->render( 'legend', title => 'travelynx: Legende' );
}
sub offline {
diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm
new file mode 100644
index 0000000..3cdeff8
--- /dev/null
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -0,0 +1,154 @@
+package Travelynx::Controller::Traewelling;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Controller';
+use Mojo::Promise;
+
+sub oauth {
+ my ($self) = @_;
+
+ if ( $self->param('action')
+ and $self->validation->csrf_protect->has_error('csrf_token') )
+ {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+
+ $self->render_later;
+
+ my $oa = $self->config->{traewelling}{oauth};
+
+ return $self->oauth2->get_token_p(
+ traewelling => {
+ redirect_uri =>
+ $self->base_url_for('/oauth/traewelling')->to_abs->scheme(
+ $self->app->mode eq 'development' ? 'http' : 'https'
+ )->to_string,
+ scope => 'read-statuses write-statuses'
+ }
+ )->then(
+ sub {
+ my ($provider) = @_;
+ if ( not defined $provider ) {
+
+ # OAuth2 plugin performed a redirect, no need to render
+ return;
+ }
+ if ( not $provider or not $provider->{access_token} ) {
+ $self->flash( new_traewelling => 1 );
+ $self->flash( login_error => 'no token received' );
+ $self->redirect_to('/account/traewelling');
+ return;
+ }
+ my $uid = $self->current_user->{id};
+ my $token = $provider->{access_token};
+ $self->traewelling->link(
+ uid => $self->current_user->{id},
+ token => $provider->{access_token},
+ refresh_token => $provider->{refresh_token},
+ expires_in => $provider->{expires_in},
+ );
+ return $self->traewelling_api->get_user_p( $uid, $token )->then(
+ sub {
+ $self->flash( new_traewelling => 1 );
+ $self->redirect_to('/account/traewelling');
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ say "error $err";
+ $self->flash( new_traewelling => 1 );
+ $self->flash( login_error => $err );
+ $self->redirect_to('/account/traewelling');
+ return;
+ }
+ );
+}
+
+sub settings {
+ my ($self) = @_;
+
+ my $uid = $self->current_user->{id};
+
+ if ( $self->param('action')
+ and $self->validation->csrf_protect->has_error('csrf_token') )
+ {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+
+ if ( $self->param('action') and $self->param('action') eq 'logout' ) {
+ $self->render_later;
+ my $traewelling = $self->traewelling->get( uid => $uid );
+ $self->traewelling_api->logout_p(
+ uid => $uid,
+ token => $traewelling->{token}
+ )->then(
+ sub {
+ $self->flash( success => 'traewelling' );
+ $self->redirect_to('account');
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ 'traewelling',
+ traewelling => {},
+ new_traewelling => 1,
+ logout_error => $err,
+ );
+ }
+ )->wait;
+ return;
+ }
+ elsif ( $self->param('action') and $self->param('action') eq 'config' ) {
+ $self->traewelling->set_sync(
+ uid => $uid,
+ push_sync => $self->param('sync_source') eq 'travelynx' ? 1 : 0,
+ pull_sync => $self->param('sync_source') eq 'traewelling' ? 1 : 0,
+ toot => $self->param('toot') ? 1 : 0,
+ tweet => $self->param('tweet') ? 1 : 0,
+ );
+ $self->flash( success => 'traewelling' );
+ $self->redirect_to('account');
+ return;
+ }
+
+ my $traewelling = $self->traewelling->get( uid => $uid );
+
+ if ( $traewelling->{push_sync} ) {
+ $self->param( sync_source => 'travelynx' );
+ }
+ elsif ( $traewelling->{pull_sync} ) {
+ $self->param( sync_source => 'traewelling' );
+ }
+ else {
+ $self->param( sync_source => 'none' );
+ }
+ if ( $traewelling->{data}{toot} ) {
+ $self->param( toot => 1 );
+ }
+ if ( $traewelling->{data}{tweet} ) {
+ $self->param( tweet => 1 );
+ }
+
+ $self->stash( title => 'travelynx × träwelling' );
+ $self->render(
+ 'traewelling',
+ traewelling => $traewelling,
+ );
+}
+
+1;
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 6b8c766..89385e1 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,125 +1,465 @@
package Travelynx::Controller::Traveling;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use DateTime::Format::Strptime;
-use List::Util qw(uniq);
-use List::UtilsBy qw(uniq_by);
+use List::Util qw(uniq min max);
+use List::UtilsBy qw(max_by uniq_by);
use List::MoreUtils qw(first_index);
+use Mojo::Promise;
+use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
-sub homepage {
- my ($self) = @_;
- if ( $self->is_user_authenticated ) {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- with_autocomplete => 1,
- with_geolocation => 1
- );
- $self->mark_seen( $self->current_user->{id} );
- }
- else {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- intro => 1
- );
+# Internal Helpers
+
+sub has_str_in_list {
+ my ( $str, @strs ) = @_;
+ if ( List::Util::any { $str eq $_ } @strs ) {
+ return 1;
}
+ return;
}
-sub user_status {
- my ($self) = @_;
+sub get_connecting_trains_p {
+ my ( $self, %opt ) = @_;
- my $name = $self->stash('name');
- my $ts = $self->stash('ts');
- my $user = $self->get_privacy_by_name($name);
+ my $uid = $opt{uid} //= $self->current_user->{id};
+ my $use_history = $self->users->use_history( uid => $uid );
- if (
- $user
- and ( $user->{public_level} & 0x02
- or
- ( $user->{public_level} & 0x01 and $self->is_user_authenticated ) )
- )
- {
- my $status = $self->get_user_status( $user->{id} );
+ my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
+ my $now = $self->now->epoch;
+ my ( $stationinfo, $arr_epoch, $arr_platform, $arr_countdown );
- my %tw_data = (
- card => 'summary',
- site => '@derfnull',
- image => $self->url_for('/static/icons/icon-512x512.png')
- ->to_abs->scheme('https'),
- );
+ my $promise = Mojo::Promise->new;
- if (
- $ts
- and ( not $status->{checked_in}
- or $status->{sched_departure}->epoch != $ts )
- )
- {
- $tw_data{title} = "Bahnfahrt beendet";
- $tw_data{description} = "${name} hat das Ziel erreicht";
- }
- elsif ( $status->{checked_in} ) {
- $tw_data{title} = "${name} ist unterwegs";
- $tw_data{description} = sprintf(
- '%s %s von %s nach %s',
- $status->{train_type},
- $status->{train_line} // $status->{train_no},
- $status->{dep_name},
- $status->{arr_name} // 'irgendwo'
- );
- if ( $status->{real_arrival}->epoch ) {
- $tw_data{description} .= $status->{real_arrival}
- ->strftime(' – Ankunft gegen %H:%M Uhr');
- }
+ if ( $opt{eva} ) {
+ if ( $use_history & 0x01 ) {
+ $eva = $opt{eva};
}
- else {
- $tw_data{title} = "${name} ist gerade nicht eingecheckt";
- $tw_data{description} = "Letztes Fahrtziel: $status->{arr_name}";
+ elsif ( $opt{destination_name} ) {
+ $eva = $opt{eva};
+ }
+ }
+ else {
+ if ( $use_history & 0x02 ) {
+ my $status = $self->get_user_status;
+ $eva = $status->{arr_eva};
+ $exclude_via = $status->{dep_name};
+ $exclude_train_id = $status->{train_id};
+ $arr_platform = $status->{arr_platform};
+ $stationinfo = $status->{extra_data}{stationinfo_arr};
+ if ( $status->{real_arrival} ) {
+ $exclude_before = $arr_epoch = $status->{real_arrival}->epoch;
+ $arr_countdown = $status->{arrival_countdown};
+ }
}
+ }
- $self->render(
- 'user_status',
- name => $name,
- public_level => $user->{public_level},
- journey => $status,
- twitter => \%tw_data,
- );
+ $exclude_before //= $now - 300;
+
+ if ( not $eva ) {
+ return $promise->reject;
}
- elsif ( $user->{public_level} & 0x01 ) {
- $self->render( 'login', redirect_to => $self->req->url );
+
+ my ( $dest_ids, $destinations )
+ = $self->journeys->get_connection_targets(%opt);
+
+ my @destinations = uniq_by { $_->{name} } @{$destinations};
+
+ if ($exclude_via) {
+ @destinations = grep { $_->{name} ne $exclude_via } @destinations;
}
- else {
- $self->render('not_found');
+
+ if ( not @destinations ) {
+ return $promise->reject;
}
-}
-sub public_status_card {
- my ($self) = @_;
+ my $iris_eva = $eva;
+ if ( $eva < 8000000 ) {
+ $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} )
+ // $eva;
+ }
- my $name = $self->stash('name');
- my $user = $self->get_privacy_by_name($name);
+ my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0;
+ my $lookahead
+ = $can_check_in ? 40 : ( ( ${arr_countdown} // 0 ) / 60 + 40 );
- delete $self->stash->{layout};
+ my $iris_promise = Mojo::Promise->new;
+ my %via_count = map { $_->{name} => 0 } @destinations;
- if (
- $user
- and ( $user->{public_level} & 0x02
- or
- ( $user->{public_level} & 0x01 and $self->is_user_authenticated ) )
- )
+ if ( $iris_eva >= 8000000
+ and List::Util::any { $_->{eva} >= 8000000 } @destinations )
{
- my $status = $self->get_user_status( $user->{id} );
+ $self->iris->get_departures_p(
+ station => $iris_eva,
+ lookbehind => 10,
+ lookahead => $lookahead,
+ with_related => 1
+ )->then(
+ sub {
+ my ($stationboard) = @_;
+ if ( $stationboard->{errstr} ) {
+ $iris_promise->resolve( [] );
+ return;
+ }
+
+ @{ $stationboard->{results} } = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
+ @{ $stationboard->{results} };
+ my @results;
+ my @cancellations;
+ my $excluded_train;
+ for my $train ( @{ $stationboard->{results} } ) {
+ if ( not $train->departure ) {
+ next;
+ }
+ if ( $exclude_before
+ and $train->departure
+ and $train->departure->epoch < $exclude_before )
+ {
+ next;
+ }
+ if ( $exclude_train_id
+ and $train->train_id eq $exclude_train_id )
+ {
+ $excluded_train = $train;
+ next;
+ }
+
+ # In general, this function is meant to return feasible
+ # connections. However, cancelled connections may also be of
+ # interest and are also useful for logging cancellations.
+ # To satisfy both demands with (hopefully) little confusion and
+ # UI clutter, this function returns two concatenated arrays:
+ # actual connections (ordered by actual departure time) followed
+ # by cancelled connections (ordered by scheduled departure time).
+ # This is easiest to achieve in two separate loops.
+ #
+ # Note that a cancelled train may still have a matching destination
+ # in its route_post, e.g. if it leaves out $eva due to
+ # unscheduled route changes but continues on schedule afterwards
+ # -- so it is only cancelled at $eva, not on the remainder of
+ # the route. Also note that this specific case is not yet handled
+ # properly by the cancellation logic etc.
+
+ if ( $train->departure_is_cancelled ) {
+ my @via = (
+ $train->sched_route_post, $train->sched_route_end
+ );
+ for my $dest (@destinations) {
+ if ( has_str_in_list( $dest->{name}, @via ) ) {
+ push( @cancellations, [ $train, $dest ] );
+ next;
+ }
+ }
+ }
+ else {
+ my @via = ( $train->route_post, $train->route_end );
+ for my $dest (@destinations) {
+ if ( $via_count{ $dest->{name} } < 2
+ and has_str_in_list( $dest->{name}, @via ) )
+ {
+ push( @results, [ $train, $dest ] );
+
+ # Show all past and up to two future departures per destination
+ if ( not $train->departure
+ or $train->departure->epoch >= $now )
+ {
+ $via_count{ $dest->{name} }++;
+ }
+ next;
+ }
+ }
+ }
+ }
+
+ @results = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map {
+ [
+ $_,
+ $_->[0]->departure->epoch
+ // $_->[0]->sched_departure->epoch
+ ]
+ } @results;
+ @cancellations = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map { [ $_, $_->[0]->sched_departure->epoch ] }
+ @cancellations;
+
+ # remove trains whose route matches the excluded one's
+ if ($excluded_train) {
+ my $route_pre
+ = join( '|', reverse $excluded_train->route_pre );
+ @results
+ = grep { join( '|', $_->[0]->route_post ) ne $route_pre }
+ @results;
+ my $route_post = join( '|', $excluded_train->route_post );
+ @results
+ = grep { join( '|', $_->[0]->route_post ) ne $route_post }
+ @results;
+ }
+
+ # add message IDs and 'transfer short' hints
+ for my $result (@results) {
+ my $train = $result->[0];
+ my @message_ids
+ = List::Util::uniq map { $_->[1] } $train->raw_messages;
+ $train->{message_id} = { map { $_ => 1 } @message_ids };
+ my $interchange_duration;
+ if ( exists $stationinfo->{i} ) {
+ if ( defined $arr_platform
+ and defined $train->platform )
+ {
+ $interchange_duration
+ = $stationinfo->{i}{$arr_platform}
+ { $train->platform };
+ }
+ $interchange_duration //= $stationinfo->{i}{"*"};
+ }
+ if ( defined $interchange_duration ) {
+ my $interchange_time
+ = ( $train->departure->epoch - $arr_epoch ) / 60;
+ if ( $interchange_time < $interchange_duration ) {
+ $train->{interchange_text} = 'Anschluss knapp';
+ $train->{interchange_icon} = 'directions_run';
+ }
+ elsif ( $interchange_time == $interchange_duration ) {
+ $train->{interchange_text}
+ = 'Anschluss könnte knapp werden';
+ $train->{interchange_icon} = 'directions_run';
+ }
+ }
+ }
+
+ $iris_promise->resolve( [ @results, @cancellations ] );
+ return;
+ }
+ )->catch(
+ sub {
+ $iris_promise->resolve( [] );
+ return;
+ }
+ )->wait;
+ }
+ else {
+ $iris_promise->resolve( [] );
+ }
+
+ my $hafas_promise = Mojo::Promise->new;
+ $self->hafas->get_departures_p(
+ eva => $eva,
+ lookbehind => 10,
+ lookahead => $lookahead
+ )->then(
+ sub {
+ my ($status) = @_;
+ $hafas_promise->resolve( [ $status->results ] );
+ return;
+ }
+ )->catch(
+ sub {
+ # HAFAS data is optional.
+ # Errors are logged by get_json_p and can be silently ignored here.
+ $hafas_promise->resolve( [] );
+ return;
+ }
+ )->wait;
+
+ Mojo::Promise->all( $iris_promise, $hafas_promise )->then(
+ sub {
+ my ( $iris, $hafas ) = @_;
+ my @iris_trains = @{ $iris->[0] };
+ my @all_hafas_trains = @{ $hafas->[0] };
+ my @hafas_trains;
+
+ # We've already got a list of connecting trains; this function
+ # only adds further information to them. We ignore errors, as
+ # partial data is better than no data.
+ eval {
+ for my $iris_train (@iris_trains) {
+ if ( $iris_train->[0]->departure_is_cancelled ) {
+ for my $hafas_train (@all_hafas_trains) {
+ if ( $hafas_train->number
+ and $hafas_train->number
+ == $iris_train->[0]->train_no )
+ {
+ $hafas_train->{iris_seen} = 1;
+ next;
+ }
+ }
+ next;
+ }
+ for my $hafas_train (@all_hafas_trains) {
+ if ( $hafas_train->number
+ and $hafas_train->number
+ == $iris_train->[0]->train_no )
+ {
+ $hafas_train->{iris_seen} = 1;
+ if ( $hafas_train->load
+ and $hafas_train->load->{SECOND} )
+ {
+ $iris_train->[3] = $hafas_train->load;
+ }
+ for my $stop ( $hafas_train->route ) {
+ if ( $stop->loc->name
+ and $stop->loc->name eq
+ $iris_train->[1]->{name}
+ and $stop->arr )
+ {
+ $iris_train->[2] = $stop->arr;
+ if ( $iris_train->[0]->departure_delay
+ and not $stop->arr_delay )
+ {
+ $iris_train->[2]
+ ->add( minutes => $iris_train->[0]
+ ->departure_delay );
+ }
+ last;
+ }
+ }
+ last;
+ }
+ }
+ }
+ for my $hafas_train (@all_hafas_trains) {
+ if ( $hafas_train->{iris_seen} ) {
+ next;
+ }
+ if ( $hafas_train->station_eva >= 8000000 ) {
+
+ # better safe than sorry, for now
+ next;
+ }
+ for my $stop ( $hafas_train->route ) {
+ for my $dest (@destinations) {
+ if ( $stop->loc->name
+ and $stop->loc->name eq $dest->{name}
+ and $via_count{ $dest->{name} } < 2
+ and $hafas_train->datetime )
+ {
+ my $departure = $hafas_train->datetime;
+ my $arrival = $stop->arr;
+ my $delay = $hafas_train->delay;
+ if ( $delay
+ and $stop->arr == $stop->sched_arr )
+ {
+ $arrival->add( minutes => $delay );
+ }
+ if ( $departure->epoch >= $exclude_before ) {
+ $via_count{ $dest->{name} }++;
+ push( @hafas_trains,
+ [ $hafas_train, $dest, $arrival ] );
+ }
+ }
+ }
+ }
+ }
+ };
+ if ($@) {
+ $self->app->log->error(
+ "get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@"
+ );
+ }
+
+ $promise->resolve( \@iris_trains, \@hafas_trains );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub compute_effective_visibility {
+ my ( $self, $default_visibility, $journey_visibility ) = @_;
+ if ( $journey_visibility eq 'default' ) {
+ return $default_visibility;
+ }
+ return $journey_visibility;
+}
+
+# Controllers
+
+sub homepage {
+ my ($self) = @_;
+ if ( $self->is_user_authenticated ) {
+ my $uid = $self->current_user->{id};
+ my $status = $self->get_user_status;
+ my @timeline = $self->in_transit->get_timeline(
+ uid => $uid,
+ short => 1
+ );
+ $self->stash( timeline => [@timeline] );
+ my @recent_targets;
+ if ( $status->{checked_in} ) {
+ my $journey_visibility
+ = $self->compute_effective_visibility(
+ $self->current_user->{default_visibility_str},
+ $status->{visibility_str} );
+ if ( defined $status->{arrival_countdown}
+ and $status->{arrival_countdown} < ( 40 * 60 ) )
+ {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ $self->users->mark_seen( uid => $uid );
+ }
+ )->catch(
+ sub {
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ );
+ $self->users->mark_seen( uid => $uid );
+ }
+ )->wait;
+ return;
+ }
+ else {
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ );
+ $self->users->mark_seen( uid => $uid );
+ return;
+ }
+ }
+ else {
+ @recent_targets = uniq_by { $_->{eva} }
+ $self->journeys->get_latest_checkout_stations( uid => $uid );
+ }
$self->render(
- '_public_status_card',
- name => $name,
- public_level => $user->{public_level},
- journey => $status
+ 'landingpage',
+ user_status => $status,
+ recent_targets => \@recent_targets,
+ with_autocomplete => 1,
+ with_geolocation => 1
);
+ $self->users->mark_seen( uid => $uid );
}
else {
- $self->render('not_found');
+ $self->render( 'landingpage', intro => 1 );
}
}
@@ -129,10 +469,93 @@ sub status_card {
delete $self->stash->{layout};
+ my @timeline = $self->in_transit->get_timeline(
+ uid => $self->current_user->{id},
+ short => 1
+ );
+ $self->stash( timeline => [@timeline] );
+
if ( $status->{checked_in} ) {
- $self->render( '_checked_in', journey => $status );
+ my $journey_visibility
+ = $self->compute_effective_visibility(
+ $self->current_user->{default_visibility_str},
+ $status->{visibility_str} );
+ if ( defined $status->{arrival_countdown}
+ and $status->{arrival_countdown} < ( 40 * 60 ) )
+ {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ }
+ )->catch(
+ sub {
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ );
+ }
+ )->wait;
+ return;
+ }
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ );
+ }
+ elsif ( $status->{cancellation} ) {
+ $self->render_later;
+ $self->get_connecting_trains_p(
+ eva => $status->{cancellation}{dep_eva},
+ destination_name => $status->{cancellation}{arr_name}
+ )->then(
+ sub {
+ my ($connecting_trains) = @_;
+ $self->render(
+ '_cancelled_departure',
+ journey => $status->{cancellation},
+ connections_iris => $connecting_trains
+ );
+ }
+ )->catch(
+ sub {
+ $self->render( '_cancelled_departure',
+ journey => $status->{cancellation} );
+ }
+ )->wait;
+ return;
}
else {
+ my @connecting_trains;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ if ( $now->epoch - $status->{timestamp}->epoch < ( 30 * 60 ) ) {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ '_checked_out',
+ journey => $status,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ }
+ )->catch(
+ sub {
+ $self->render( '_checked_out', journey => $status );
+ }
+ )->wait;
+ return;
+ }
$self->render( '_checked_out', journey => $status );
}
}
@@ -145,38 +568,71 @@ sub geolocation {
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
+ return;
}
- else {
- my @candidates = map {
- {
- ds100 => $_->[0][0],
- name => $_->[0][1],
- eva => $_->[0][2],
- lon => $_->[0][3],
- lat => $_->[0][4],
- distance => $_->[1],
- }
- } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
- $lat, 10 );
- @candidates = uniq_by { $_->{name} } @candidates;
- if ( @candidates > 5 ) {
+ $self->render_later;
+
+ my @iris = map {
+ {
+ ds100 => $_->[0][0],
+ name => $_->[0][1],
+ eva => $_->[0][2],
+ lon => $_->[0][3],
+ lat => $_->[0][4],
+ distance => $_->[1],
+ hafas => 0,
+ }
+ } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
+ $lat, 10 );
+ @iris = uniq_by { $_->{name} } @iris;
+ if ( @iris > 5 ) {
+ @iris = @iris[ 0 .. 4 ];
+ }
+
+ Travel::Status::DE::HAFAS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->ua,
+ geoSearch => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my @hafas = map {
+ {
+ name => $_->name,
+ eva => $_->eva,
+ distance => $_->distance_m / 1000,
+ hafas => 1
+ }
+ } $hafas->results;
+ if ( @hafas > 10 ) {
+ @hafas = @hafas[ 0 .. 9 ];
+ }
+ my @results = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map { [ $_, $_->{distance} ] } ( @iris, @hafas );
$self->render(
json => {
- candidates => [ @candidates[ 0 .. 4 ] ],
+ candidates => [@results],
}
);
}
- else {
+ )->catch(
+ sub {
+ my ($err) = @_;
$self->render(
json => {
- candidates => [@candidates],
+ candidates => [@iris],
+ warning => $err,
}
);
}
- }
+ )->wait;
}
-sub log_action {
+sub travel_action {
my ($self) = @_;
my $params = $self->req->json;
@@ -211,61 +667,124 @@ sub log_action {
if ( $params->{action} eq 'checkin' ) {
- my ( $train, $error )
- = $self->checkin( $params->{station}, $params->{train} );
- my $destination = $params->{dest};
+ my $status = $self->get_user_status;
+ my $promise;
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- elsif ( not $destination ) {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
+ if ( $status->{checked_in}
+ and $status->{arr_eva}
+ and $status->{arrival_countdown} <= 0 )
+ {
+ $promise = $self->checkout_p( station => $status->{arr_eva} );
}
else {
- # Silently ignore errors -- if they are permanent, the user will see
- # them when selecting the destination manually.
- my ( $still_checked_in, undef )
- = $self->checkout( $destination, 0 );
- my $station_link = '/s/' . $destination;
- $self->render(
- json => {
- success => 1,
- redirect_to => $still_checked_in ? '/' : $station_link,
- },
- );
+ $promise = Mojo::Promise->resolve;
}
+
+ $self->render_later;
+ $promise->then(
+ sub {
+ return $self->checkin_p(
+ station => $params->{station},
+ train_id => $params->{train}
+ );
+ }
+ )->then(
+ sub {
+ my $destination = $params->{dest};
+ if ( not $destination ) {
+ $self->render(
+ json => {
+ success => 1,
+ redirect_to => '/',
+ },
+ );
+ return;
+ }
+
+ # Silently ignore errors -- if they are permanent, the user will see
+ # them when selecting the destination manually.
+ return $self->checkout_p(
+ station => $destination,
+ force => 0
+ );
+ }
+ )->then(
+ sub {
+ my ( $still_checked_in, undef ) = @_;
+ if ( my $destination = $params->{dest} ) {
+ my $station_link = '/s/' . $destination;
+ if ( $status->{train_id} =~ m{[|]} ) {
+ $station_link .= '?hafas=1';
+ }
+ $self->render(
+ json => {
+ success => 1,
+ redirect_to => $still_checked_in
+ ? '/'
+ : $station_link,
+ },
+ );
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ }
+ )->wait;
}
elsif ( $params->{action} eq 'checkout' ) {
- my ( $still_checked_in, $error )
- = $self->checkout( $params->{station}, $params->{force} );
- my $station_link = '/s/' . $params->{station};
+ $self->render_later;
+ my $status = $self->get_user_status;
+ $self->checkout_p(
+ station => $params->{station},
+ force => $params->{force}
+ )->then(
+ sub {
+ my ( $still_checked_in, $error ) = @_;
+ my $station_link = '/s/' . $params->{station};
+ if ( $status->{train_id} =~ m{[|]} ) {
+ $station_link .= '?hafas=1';
+ }
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => $still_checked_in ? '/' : $station_link,
- },
- );
- }
+ if ($error) {
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ }
+ else {
+ $self->render(
+ json => {
+ success => 1,
+ redirect_to => $still_checked_in
+ ? '/'
+ : $station_link,
+ },
+ );
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ return;
+ }
+ )->wait;
}
elsif ( $params->{action} eq 'undo' ) {
my $status = $self->get_user_status;
@@ -281,7 +800,12 @@ sub log_action {
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
- $redir = '/s/' . $status->{dep_ds100};
+ if ( $status->{train_id} =~ m{[|]} ) {
+ $redir = '/s/' . $status->{dep_eva} . '?hafas=1';
+ }
+ else {
+ $redir = '/s/' . $status->{dep_ds100};
+ }
}
$self->render(
json => {
@@ -292,50 +816,77 @@ sub log_action {
}
}
elsif ( $params->{action} eq 'cancelled_from' ) {
- my ( undef, $error )
- = $self->checkin( $params->{station}, $params->{train} );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
- }
+ $self->render_later;
+ $self->checkin_p(
+ station => $params->{station},
+ train_id => $params->{train}
+ )->then(
+ sub {
+ $self->render(
+ json => {
+ success => 1,
+ redirect_to => '/',
+ },
+ );
+ }
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ }
+ )->wait;
}
elsif ( $params->{action} eq 'cancelled_to' ) {
- my ( undef, $error )
- = $self->checkout( $params->{station}, 1 );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
- }
+ $self->render_later;
+ $self->checkout_p(
+ station => $params->{station},
+ force => 1
+ )->then(
+ sub {
+ my ( undef, $error ) = @_;
+ if ($error) {
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ }
+ else {
+ $self->render(
+ json => {
+ success => 1,
+ redirect_to => '/',
+ },
+ );
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->render(
+ json => {
+ success => 0,
+ error => $error,
+ },
+ );
+ return;
+ }
+ )->wait;
}
elsif ( $params->{action} eq 'delete' ) {
- my $error = $self->delete_journey( $params->{id}, $params->{checkin},
- $params->{checkout} );
+ my $error = $self->journeys->delete(
+ uid => $self->current_user->{id},
+ id => $params->{id},
+ checkin => $params->{checkin},
+ checkout => $params->{checkout}
+ );
if ($error) {
$self->render(
json => {
@@ -364,57 +915,277 @@ sub log_action {
}
sub station {
- my ($self) = @_;
- my $station = $self->stash('station');
- my $train = $self->param('train');
+ my ($self) = @_;
+ my $station = $self->stash('station');
+ my $train = $self->param('train');
+ my $trip_id = $self->param('trip_id');
+ my $timestamp = $self->param('timestamp');
+ my $uid = $self->current_user->{id};
+
+ my @timeline = $self->in_transit->get_timeline(
+ uid => $uid,
+ short => 1
+ );
+ my %checkin_by_train;
+ for my $checkin (@timeline) {
+ say $checkin->{train_id};
+ push( @{ $checkin_by_train{ $checkin->{train_id} } }, $checkin );
+ }
+ $self->stash( checkin_by_train => \%checkin_by_train );
- my $status = $self->get_departures( $station, 120, 30, 1 );
+ $self->render_later;
- if ( $status->{errstr} ) {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- with_autocomplete => 1,
- with_geolocation => 1,
- error => $status->{errstr}
+ if ( $timestamp and $timestamp =~ m{ ^ \d+ $ }x ) {
+ $timestamp = DateTime->from_epoch(
+ epoch => $timestamp,
+ time_zone => 'Europe/Berlin'
);
}
else {
- # You can't check into a train which terminates here
- my @results = grep { $_->departure } @{ $status->{results} };
-
- @results = map { $_->[0] }
- sort { $b->[1] <=> $a->[1] }
- map { [ $_, $_->departure->epoch // $_->sched_departure->epoch ] }
- @results;
-
- if ($train) {
- @results
- = grep { $_->type . ' ' . $_->train_no eq $train } @results;
- }
+ $timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
+ }
- $self->render(
- 'departures',
- ds100 => $status->{station_ds100},
- results => \@results,
- station => $status->{station_name},
- related_stations => $status->{related_stations},
- title => "travelynx: $status->{station_name}",
+ my $use_hafas = $self->param('hafas');
+ my $promise;
+ if ($use_hafas) {
+ $promise = $self->hafas->get_departures_p(
+ eva => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ lookahead => 30,
);
}
- $self->mark_seen( $self->current_user->{id} );
+ else {
+ $promise = $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 120,
+ lookahead => 30,
+ with_related => 1,
+ );
+ }
+ $promise->then(
+ sub {
+ my ($status) = @_;
+ my $api_link;
+ my @results;
+
+ my $now = $self->now->epoch;
+ my $now_within_range
+ = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
+
+ if ($use_hafas) {
+
+ my $iris_eva = List::Util::min grep { $_ >= 1000000 }
+ @{ $status->station->{evas} // [] };
+ if ($iris_eva) {
+ $api_link = '/s/' . $iris_eva;
+ }
+
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->datetime->epoch ] } $status->results;
+ $self->stations->add_meta(
+ eva => $status->station->{eva},
+ meta => $status->station->{evas} // []
+ );
+ $status = {
+ station_eva => $status->station->{eva},
+ station_name => (
+ List::Util::reduce { length($a) < length($b) ? $a : $b }
+ @{ $status->station->{names} }
+ ),
+ related_stations => [],
+ };
+ }
+ else {
+
+ $api_link = '/s/' . $status->{station_eva} . '?hafas=1';
+
+ # You can't check into a train which terminates here
+ @results = grep { $_->departure } @{ $status->{results} };
+
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map {
+ [ $_, $_->departure->epoch // $_->sched_departure->epoch ]
+ } @results;
+ }
+
+ my $user_status = $self->get_user_status;
+
+ my $can_check_out = 0;
+ if ( $user_status->{checked_in} ) {
+ for my $stop ( @{ $user_status->{route_after} } ) {
+ if (
+ $stop->[1] eq $status->{station_eva}
+ or List::Util::any { $stop->[1] eq $_->{uic} }
+ @{ $status->{related_stations} }
+ )
+ {
+ $can_check_out = 1;
+ last;
+ }
+ }
+ }
+
+ my $connections_p;
+ if ( $trip_id and $use_hafas ) {
+ @results = grep { $_->id eq $trip_id } @results;
+ }
+ elsif ( $train and not $use_hafas ) {
+ @results
+ = grep { $_->type . ' ' . $_->train_no eq $train } @results;
+ }
+ else {
+ if ( $user_status->{cancellation}
+ and $status->{station_eva} eq
+ $user_status->{cancellation}{dep_eva} )
+ {
+ $connections_p = $self->get_connecting_trains_p(
+ eva => $user_status->{cancellation}{dep_eva},
+ destination_name =>
+ $user_status->{cancellation}{arr_name}
+ );
+ }
+ else {
+ $connections_p = $self->get_connecting_trains_p(
+ eva => $status->{station_eva} );
+ }
+ }
+
+ if ($connections_p) {
+ $connections_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ 'departures',
+ eva => $status->{station_eva},
+ datetime => $timestamp,
+ now_in_range => $now_within_range,
+ results => \@results,
+ hafas => $use_hafas,
+ station => $status->{station_name},
+ related_stations => $status->{related_stations},
+ user_status => $user_status,
+ can_check_out => $can_check_out,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ api_link => $api_link,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ )->catch(
+ sub {
+ $self->render(
+ 'departures',
+ eva => $status->{station_eva},
+ datetime => $timestamp,
+ now_in_range => $now_within_range,
+ results => \@results,
+ hafas => $use_hafas,
+ station => $status->{station_name},
+ related_stations => $status->{related_stations},
+ user_status => $user_status,
+ can_check_out => $can_check_out,
+ api_link => $api_link,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ )->wait;
+ }
+ else {
+ $self->render(
+ 'departures',
+ eva => $status->{station_eva},
+ datetime => $timestamp,
+ now_in_range => $now_within_range,
+ results => \@results,
+ hafas => $use_hafas,
+ station => $status->{station_name},
+ related_stations => $status->{related_stations},
+ user_status => $user_status,
+ can_check_out => $can_check_out,
+ api_link => $api_link,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ }
+ )->catch(
+ sub {
+ my ( $err, $status ) = @_;
+ if ( $status and $status->{suggestions} ) {
+ $self->render(
+ 'disambiguation',
+ suggestions => $status->{suggestions},
+ status => 300,
+ );
+ }
+ elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' )
+ {
+ $self->hafas->search_location_p( query => $station )->then(
+ sub {
+ my ($hafas2) = @_;
+ my @suggestions = $hafas2->results;
+ if ( @suggestions == 1 ) {
+ $self->redirect_to(
+ '/s/' . $suggestions[0]->eva . '?hafas=1' );
+ }
+ else {
+ $self->render(
+ 'disambiguation',
+ suggestions => [
+ map { { name => $_->name, eva => $_->eva } }
+ @suggestions
+ ],
+ status => 300,
+ );
+ }
+ }
+ )->catch(
+ sub {
+ my ($err2) = @_;
+ $self->render(
+ 'exception',
+ exception =>
+"locationSearch threw '$err2' when handling '$err'",
+ status => 502
+ );
+ }
+ )->wait;
+ }
+ else {
+ $self->render(
+ 'exception',
+ exception => $err,
+ status => 502
+ );
+ }
+ }
+ )->wait;
+ $self->users->mark_seen( uid => $uid );
}
sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
- $self->redirect_to("/s/${station}");
+ if ( my $s = $self->app->stations->search($station) ) {
+ if ( $s->{source} == 1 ) {
+ $self->redirect_to("/s/${station}?hafas=1");
+ }
+ else {
+ $self->redirect_to("/s/${station}");
+ }
+ }
+ else {
+ $self->redirect_to("/s/${station}?hafas=1");
+ }
}
sub cancelled {
my ($self) = @_;
- my @journeys = $self->get_user_travels(
+ my @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
cancelled => 1,
with_datetime => 1
);
@@ -431,7 +1202,122 @@ sub cancelled {
sub history {
my ($self) = @_;
- $self->render( template => 'history' );
+ $self->render(
+ template => 'history',
+ title => 'travelynx: History'
+ );
+}
+
+sub commute {
+ my ($self) = @_;
+
+ my $year = $self->param('year');
+ my $filter_type = $self->param('filter_type') || 'exact';
+ my $station = $self->param('station');
+
+ # DateTime is very slow when looking far into the future due to DST changes
+ # -> Limit time range to avoid accidental DoS.
+ if (
+ not( $year
+ and $year =~ m{ ^ [0-9]{4} $ }x
+ and $year > 1990
+ and $year < 2100 )
+ )
+ {
+ $year = DateTime->now( time_zone => 'Europe/Berlin' )->year - 1;
+ }
+ my $interval_start = DateTime->new(
+ time_zone => 'Europe/Berlin',
+ year => $year,
+ month => 1,
+ day => 1,
+ hour => 0,
+ minute => 0,
+ second => 0,
+ );
+ my $interval_end = $interval_start->clone->add( years => 1 );
+
+ my @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ after => $interval_start,
+ before => $interval_end,
+ with_datetime => 1,
+ );
+
+ if ( not $station ) {
+ my %candidate_count;
+ for my $journey (@journeys) {
+ my $dep = $journey->{rt_departure};
+ my $arr = $journey->{rt_arrival};
+ if ( $arr->dow <= 5 and $arr->hour <= 12 ) {
+ $candidate_count{ $journey->{to_name} }++;
+ }
+ elsif ( $dep->dow <= 5 and $dep->hour > 12 ) {
+ $candidate_count{ $journey->{from_name} }++;
+ }
+ else {
+ # Avoid selecting an intermediate station for multi-leg commutes.
+ # Assumption: The intermediate station is also used for private
+ # travels -> penalize stations which are used on weekends or at
+ # unexpected times.
+ $candidate_count{ $journey->{from_name} }--;
+ $candidate_count{ $journey->{to_name} }--;
+ }
+ }
+ $station = max_by { $candidate_count{$_} } keys %candidate_count;
+ }
+
+ my %journeys_by_month;
+ my %count_by_month;
+ my $total = 0;
+
+ my $prev_doy = 0;
+ for my $journey ( reverse @journeys ) {
+ my $month = $journey->{rt_departure}->month;
+ if (
+ (
+ $filter_type eq 'exact' and ( $journey->{to_name} eq $station
+ or $journey->{from_name} eq $station )
+ )
+ or (
+ $filter_type eq 'substring'
+ and ( $journey->{to_name} =~ m{\Q$station\E}
+ or $journey->{from_name} =~ m{\Q$station\E} )
+ )
+ or (
+ $filter_type eq 'regex'
+ and ( $journey->{to_name} =~ m{$station}
+ or $journey->{from_name} =~ m{$station} )
+ )
+ )
+ {
+ push( @{ $journeys_by_month{$month} }, $journey );
+
+ my $doy = $journey->{rt_departure}->day_of_year;
+ if ( $doy != $prev_doy ) {
+ $count_by_month{$month}++;
+ $total++;
+ }
+
+ $prev_doy = $doy;
+ }
+ }
+
+ $self->param( year => $year );
+ $self->param( filter_type => $filter_type );
+ $self->param( station => $station );
+
+ $self->render(
+ template => 'commute',
+ with_autocomplete => 1,
+ journeys_by_month => \%journeys_by_month,
+ count_by_month => \%count_by_month,
+ total_journeys => $total,
+ title => 'travelynx: Reisen nach Station',
+ months => [
+ qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
+ ],
+ );
}
sub map_history {
@@ -439,134 +1325,278 @@ sub map_history {
my $location = $self->app->coordinates_by_station;
- my @journeys = $self->get_user_travels;
+ if ( not $self->param('route_type') ) {
+ $self->param( route_type => 'polybee' );
+ }
+
+ my $route_type = $self->param('route_type');
+ my $filter_from = $self->param('filter_from');
+ my $filter_until = $self->param('filter_to');
+ my $filter_type = $self->param('filter_type');
+ my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
+
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+
+ if ( $filter_from
+ and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
+ {
+ $filter_from = $parser->parse_datetime($filter_from);
+ }
+ else {
+ $filter_from = undef;
+ }
+
+ if ( $filter_until
+ and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
+ {
+ $filter_until = $parser->parse_datetime($filter_until)->set(
+ hour => 23,
+ minute => 59,
+ second => 58
+ );
+ }
+ else {
+ $filter_until = undef;
+ }
+
+ my $year;
+ if ( $filter_from
+ and $filter_from->day == 1
+ and $filter_from->month == 1
+ and $filter_until
+ and $filter_until->day == 31
+ and $filter_until->month == 12
+ and $filter_from->year == $filter_until->year )
+ {
+ $year = $filter_from->year;
+ }
+
+ my @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ with_polyline => $with_polyline,
+ after => $filter_from,
+ before => $filter_until,
+ );
+
+ if ($filter_type) {
+ my @filter = split( qr{, *}, $filter_type );
+ @journeys
+ = grep { has_str_in_list( $_->{type}, @filter ) } @journeys;
+ }
if ( not @journeys ) {
$self->render(
template => 'history_map',
with_map => 1,
+ skipped_journeys => [],
station_coordinates => [],
- station_pairs => [],
+ polyline_groups => [],
);
return;
}
my $include_manual = $self->param('include_manual') ? 1 : 0;
- my $first_departure = $journeys[-1]->{rt_departure};
- my $last_departure = $journeys[0]->{rt_departure};
+ my $res = $self->journeys_to_map_data(
+ journeys => \@journeys,
+ route_type => $route_type,
+ include_manual => $include_manual
+ );
- my @stations = uniq map { $_->{to_name} } @journeys;
- push( @stations, uniq map { $_->{from_name} } @journeys );
- @stations = uniq @stations;
- my @station_coordinates = map { [ $location->{$_}, $_ ] }
- grep { exists $location->{$_} } @stations;
+ $self->render(
+ template => 'history_map',
+ year => $year,
+ with_map => 1,
+ title => 'travelynx: Karte',
+ %{$res}
+ );
+}
- my @station_pairs;
- my %seen;
+sub json_history {
+ my ($self) = @_;
+
+ $self->render(
+ json => [ $self->journeys->get( uid => $self->current_user->{id} ) ] );
+}
- for my $journey (@journeys) {
+sub csv_history {
+ my ($self) = @_;
- my @route = map { $_->[0] } @{ $journey->{route} };
- my $from_index = first_index { $_ eq $journey->{from_name} } @route;
- my $to_index = first_index { $_ eq $journey->{to_name} } @route;
+ my $csv = Text::CSV->new( { eol => "\r\n" } );
+ 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'
+ );
+ $buf .= $csv->string;
- if ( $from_index == -1
- or $to_index == -1 )
- {
- next;
- }
-
- # Manual journey entries are only included if one of the following
- # conditions is satisfied:
- # * their route has more than two elements (-> probably more than just
- # start and stop station), or
- # * $include_manual is true (-> user wants to see incomplete routes)
- # This avoids messing up the map in case an A -> B connection has been
- # tracked both with a regular checkin (-> detailed route shown on map)
- # and entered manually (-> beeline also shown on map, typically
- # significantly differs from detailed route) -- unless the user
- # sets include_manual, of course.
- if ( $journey->{edited} & 0x0010
- and @route <= 2
- and not $include_manual )
+ for my $journey (
+ $self->journeys->get(
+ uid => $self->current_user->{id},
+ with_datetime => 1
+ )
+ )
+ {
+ if (
+ $csv->combine(
+ $journey->{type},
+ $journey->{line},
+ $journey->{no},
+ $journey->{from_name},
+ $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->{user_data}{comment} // q{},
+ $journey->{id}
+ )
+ )
{
- next;
+ $buf .= $csv->string;
}
+ }
- @route = @route[ $from_index .. $to_index ];
-
- my $key = join( '|', @route );
+ $self->render(
+ text => $buf,
+ format => 'csv'
+ );
+}
- if ( $seen{$key} ) {
- next;
- }
+sub year_in_review {
+ my ($self) = @_;
+ my $year = $self->stash('year');
+ my @journeys;
- $seen{$key} = 1;
+ # DateTime is very slow when looking far into the future due to DST changes
+ # -> Limit time range to avoid accidental DoS.
+ if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )
+ {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
- # direction does not matter at the moment
- $seen{ join( '|', reverse @route ) } = 1;
+ my $interval_start = DateTime->new(
+ time_zone => 'Europe/Berlin',
+ year => $year,
+ month => 1,
+ day => 1,
+ hour => 0,
+ minute => 0,
+ second => 0,
+ );
+ my $interval_end = $interval_start->clone->add( years => 1 );
+ @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ after => $interval_start,
+ before => $interval_end,
+ with_datetime => 1
+ );
- my $prev_station = shift @route;
- for my $station (@route) {
- push( @station_pairs, [ $prev_station, $station ] );
- $prev_station = $station;
- }
+ if ( not @journeys ) {
+ $self->render(
+ 'not_found',
+ message => 'Keine Zugfahrten im angefragten Jahr gefunden.',
+ status => 404
+ );
+ return;
}
- @station_pairs = uniq_by { $_->[0] . '|' . $_->[1] } @station_pairs;
- @station_pairs
- = grep { exists $location->{ $_->[0] } and exists $location->{ $_->[1] } }
- @station_pairs;
- @station_pairs
- = map { [ $location->{ $_->[0] }, $location->{ $_->[1] } ] }
- @station_pairs;
+ my $now = $self->now;
+ if (
+ not( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) )
+ {
+ $self->render(
+ 'not_found',
+ message =>
+'Der aktuelle Jahresrückblick wird erst zum Jahresende (am 31.12.) freigeschaltet',
+ status => 404
+ );
+ return;
+ }
- my @routes;
+ my ( $stats, $review ) = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year,
+ review => 1
+ );
$self->render(
- template => 'history_map',
- with_map => 1,
- station_coordinates => \@station_coordinates,
- station_pairs => \@station_pairs,
+ 'year_in_review',
+ title => "travelynx: Jahresrückblick $year",
+ year => $year,
+ stats => $stats,
+ review => $review,
);
-}
-
-sub json_history {
- my ($self) = @_;
- $self->render( json => [ $self->get_user_travels ] );
}
sub yearly_history {
my ($self) = @_;
- my $year = $self->stash('year');
+ my $year = $self->stash('year');
+ my $filter = $self->param('filter');
my @journeys;
- my $stats;
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )
{
- @journeys = $self->get_user_travels( with_datetime => 1 );
+ $self->render( 'not_found', status => 404 );
+ return;
}
- else {
- my $interval_start = DateTime->new(
- time_zone => 'Europe/Berlin',
- year => $year,
- month => 1,
- day => 1,
- hour => 0,
- minute => 0,
- second => 0,
- );
- my $interval_end = $interval_start->clone->add( years => 1 );
- @journeys = $self->get_user_travels(
- after => $interval_start,
- before => $interval_end,
- with_datetime => 1
+ my $interval_start = DateTime->new(
+ time_zone => 'Europe/Berlin',
+ year => $year,
+ month => 1,
+ day => 1,
+ hour => 0,
+ minute => 0,
+ second => 0,
+ );
+ my $interval_end = $interval_start->clone->add( years => 1 );
+ @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ after => $interval_start,
+ before => $interval_end,
+ with_datetime => 1
+ );
+
+ if ( $filter and $filter eq 'single' ) {
+ @journeys = $self->journeys->grep_single(@journeys);
+ }
+
+ if ( not @journeys ) {
+ $self->render(
+ 'not_found',
+ status => 404,
+ message => 'Keine Zugfahrten im angefragten Jahr gefunden.'
);
- $stats = $self->get_journey_stats( year => $year );
+ return;
+ }
+
+ my $stats = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year
+ );
+
+ my $with_review;
+ my $now = $self->now;
+ if ( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) {
+ $with_review = 1;
}
$self->respond_to(
@@ -577,10 +1607,12 @@ sub yearly_history {
}
},
any => {
- template => 'history_by_year',
- journeys => [@journeys],
- year => $year,
- statistics => $stats
+ template => 'history_by_year',
+ title => "travelynx: $year",
+ journeys => [@journeys],
+ year => $year,
+ have_review => $with_review,
+ statistics => $stats
}
);
@@ -591,7 +1623,6 @@ sub monthly_history {
my $year = $self->stash('year');
my $month = $self->stash('month');
my @journeys;
- my $stats;
my @months
= (
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
@@ -606,30 +1637,43 @@ sub monthly_history {
and $month < 13 )
)
{
- @journeys = $self->get_user_travels( with_datetime => 1 );
+ $self->render( 'not_found', status => 404 );
+ return;
}
- else {
- my $interval_start = DateTime->new(
- time_zone => 'Europe/Berlin',
- year => $year,
- month => $month,
- day => 1,
- hour => 0,
- minute => 0,
- second => 0,
- );
- my $interval_end = $interval_start->clone->add( months => 1 );
- @journeys = $self->get_user_travels(
- after => $interval_start,
- before => $interval_end,
- with_datetime => 1
- );
- $stats = $self->get_journey_stats(
- year => $year,
- month => $month
+ my $interval_start = DateTime->new(
+ time_zone => 'Europe/Berlin',
+ year => $year,
+ month => $month,
+ day => 1,
+ hour => 0,
+ minute => 0,
+ second => 0,
+ );
+ my $interval_end = $interval_start->clone->add( months => 1 );
+ @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ after => $interval_start,
+ before => $interval_end,
+ with_datetime => 1
+ );
+
+ if ( not @journeys ) {
+ $self->render(
+ 'not_found',
+ message => 'Keine Zugfahrten im angefragten Monat gefunden.',
+ status => 404
);
+ return;
}
+ my $stats = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year,
+ month => $month
+ );
+
+ my $month_name = $months[ $month - 1 ];
+
$self->respond_to(
json => {
json => {
@@ -639,10 +1683,11 @@ sub monthly_history {
},
any => {
template => 'history_by_month',
+ title => "travelynx: $month_name $year",
journeys => [@journeys],
year => $year,
month => $month,
- month_name => $months[ $month - 1 ],
+ month_name => $month_name,
statistics => $stats
}
);
@@ -653,36 +1698,89 @@ sub journey_details {
my ($self) = @_;
my $journey_id = $self->stash('id');
- my $uid = $self->current_user->{id};
+ my $user = $self->current_user;
+ my $uid = $user->{id};
$self->param( journey_id => $journey_id );
- if ( not($journey_id) ) {
+ if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'journey',
+ status => 404,
error => 'notfound',
journey => {}
);
return;
}
- my $journey = $self->get_journey(
- uid => $uid,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
+ my $journey = $self->journeys->get_single(
+ uid => $uid,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
);
if ($journey) {
+ my $map_data = $self->journeys_to_map_data(
+ journeys => [$journey],
+ include_manual => 1,
+ );
+ my $with_share;
+ my $share_text;
+
+ my $visibility
+ = $self->compute_effective_visibility(
+ $user->{default_visibility_str},
+ $journey->{visibility_str} );
+
+ if ( $visibility eq 'public'
+ or $visibility eq 'travelynx'
+ or $visibility eq 'followers'
+ or $visibility eq 'unlisted' )
+ {
+ my $delay = 'pünktlich ';
+ if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) {
+ $delay = sprintf(
+ 'mit %+d ',
+ (
+ $journey->{rt_arrival}->epoch
+ - $journey->{sched_arrival}->epoch
+ ) / 60
+ );
+ }
+ $with_share = 1;
+ $share_text
+ = $journey->{km_route}
+ ? sprintf( '%.0f km', $journey->{km_route} )
+ : 'Fahrt';
+ $share_text .= sprintf( ' mit %s %s – Ankunft %sum %s',
+ $journey->{type}, $journey->{no},
+ $delay, $journey->{rt_arrival}->strftime('%H:%M') );
+ }
+
$self->render(
'journey',
- error => undef,
- journey => $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 => {}
);
@@ -690,6 +1788,94 @@ sub journey_details {
}
+sub visibility_form {
+ my ($self) = @_;
+ my $dep_ts = $self->param('dep_ts');
+ my $journey_id = $self->param('id');
+ my $action = $self->param('action') // 'none';
+ my $user = $self->current_user;
+ my $user_level = $user->{default_visibility_str};
+ my $uid = $user->{id};
+ my $status = $self->get_user_status;
+ my $visibility = $status->{visibility_str};
+ my $journey;
+
+ if ($journey_id) {
+ $journey = $self->journeys->get_single(
+ uid => $uid,
+ journey_id => $journey_id,
+ with_datetime => 1,
+ with_visibility => 1,
+ );
+ $visibility = $journey->{visibility_str};
+ }
+
+ if ( $action eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ }
+ elsif ( $dep_ts and $dep_ts != $status->{sched_departure}->epoch ) {
+ $self->render(
+ 'edit_visibility',
+ error => 'old',
+ user_level => $user_level,
+ journey => {}
+ );
+ }
+ else {
+ if ($dep_ts) {
+ $self->in_transit->update_visibility(
+ uid => $uid,
+ visibility => $self->param('status_level'),
+ );
+ $self->redirect_to('/');
+ $self->run_hook( $uid, 'update' );
+ }
+ elsif ($journey_id) {
+ $self->journeys->update_visibility(
+ uid => $uid,
+ id => $journey_id,
+ visibility => $self->param('status_level'),
+ );
+ $self->redirect_to( '/journey/' . $journey_id );
+ }
+ }
+ return;
+ }
+
+ $self->param( status_level => $visibility );
+
+ if ($journey_id) {
+ $self->render(
+ 'edit_visibility',
+ error => undef,
+ user_level => $user_level,
+ journey => $journey
+ );
+ }
+ elsif ( $status->{checked_in} ) {
+ $self->param( dep_ts => $status->{sched_departure}->epoch );
+ $self->render(
+ 'edit_visibility',
+ error => undef,
+ user_level => $user_level,
+ journey => $status
+ );
+ }
+ else {
+ $self->render(
+ 'edit_visibility',
+ error => 'notfound',
+ user_level => $user_level,
+ journey => {}
+ );
+ }
+}
+
sub comment_form {
my ($self) = @_;
my $dep_ts = $self->param('dep_ts');
@@ -730,8 +1916,13 @@ sub comment_form {
}
else {
$self->app->log->debug("set comment");
- $self->update_in_transit_comment( $self->param('comment') );
+ my $uid = $self->current_user->{id};
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ user_data => { comment => $self->param('comment') }
+ );
$self->redirect_to('/');
+ $self->run_hook( $uid, 'update' );
}
}
@@ -743,13 +1934,14 @@ sub edit_journey {
if ( not( $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'edit_journey',
+ status => 404,
error => 'notfound',
journey => {}
);
return;
}
- my $journey = $self->get_journey(
+ my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
verbose => 1,
@@ -759,6 +1951,7 @@ sub edit_journey {
if ( not $journey ) {
$self->render(
'edit_journey',
+ status => 404,
error => 'notfound',
journey => {}
);
@@ -781,8 +1974,27 @@ sub edit_journey {
{
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
- $error = $self->update_journey_part( $db, $journey->{id},
- $key, $datetime );
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ $key => $datetime
+ );
+ if ($error) {
+ last;
+ }
+ }
+ }
+ for my $key (qw(from_name to_name)) {
+ if ( defined $self->param($key)
+ and $self->param($key) ne $journey->{$key} )
+ {
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ $key => $self->param($key)
+ );
if ($error) {
last;
}
@@ -795,8 +2007,12 @@ sub edit_journey {
or $journey->{user_data}{$key} ne $self->param($key) )
)
{
- $error = $self->update_journey_part( $db, $journey->{id}, $key,
- $self->param($key) );
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ $key => $self->param($key)
+ );
if ($error) {
last;
}
@@ -807,30 +2023,36 @@ sub edit_journey {
my @route_new = split( qr{\r?\n\r?}, $self->param('route') );
@route_new = grep { $_ ne '' } @route_new;
if ( join( '|', @route_old ) ne join( '|', @route_new ) ) {
- $error
- = $self->update_journey_part( $db, $journey->{id}, 'route',
- [@route_new] );
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ route => [@route_new]
+ );
}
}
{
- my $cancelled_old = $journey->{cancelled};
+ my $cancelled_old = $journey->{cancelled} // 0;
my $cancelled_new = $self->param('cancelled') // 0;
if ( $cancelled_old != $cancelled_new ) {
- $error
- = $self->update_journey_part( $db, $journey->{id},
- 'cancelled', $cancelled_new );
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ cancelled => $cancelled_new
+ );
}
}
if ( not $error ) {
- $journey = $self->get_journey(
+ $journey = $self->journeys->get_single(
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
- $error = $self->journey_sanity_check($journey);
+ $error = $self->journeys->sanity_check($journey);
}
if ( not $error ) {
$tx->commit;
@@ -849,7 +2071,9 @@ sub edit_journey {
$self->param(
route => join( "\n", map { $_->[0] } @{ $journey->{route} } ) );
- $self->param( cancelled => $journey->{cancelled} );
+ $self->param( cancelled => $journey->{cancelled} ? 1 : 0 );
+ $self->param( from_name => $journey->{from_name} );
+ $self->param( to_name => $journey->{to_name} );
for my $key (qw(comment)) {
if ( $journey->{user_data} and $journey->{user_data}{$key} ) {
@@ -859,8 +2083,9 @@ sub edit_journey {
$self->render(
'edit_journey',
- error => $error,
- journey => $journey
+ with_autocomplete => 1,
+ error => $error,
+ journey => $journey
);
}
@@ -887,7 +2112,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
- error =>
+ error =>
'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
@@ -923,18 +2148,19 @@ sub add_journey_form {
my $db = $self->pg->db;
my $tx = $db->begin;
- $opt{db} = $db;
+ $opt{db} = $db;
+ $opt{uid} = $self->current_user->{id};
- my ( $journey_id, $error ) = $self->add_journey(%opt);
+ my ( $journey_id, $error ) = $self->journeys->add(%opt);
if ( not $error ) {
- my $journey = $self->get_journey(
+ my $journey = $self->journeys->get_single(
uid => $self->current_user->{id},
db => $db,
journey_id => $journey_id,
verbose => 1
);
- $error = $self->journey_sanity_check($journey);
+ $error = $self->journeys->sanity_check($journey);
}
if ($error) {
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
new file mode 100644
index 0000000..b98a372
--- /dev/null
+++ b/lib/Travelynx/Helper/DBDB.pm
@@ -0,0 +1,150 @@
+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, $ts, $train_no ) = @_;
+ my $api_ts = $ts->strftime('%Y%m%d%H%M');
+ my $url
+ = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
+ my $cache = $self->{cache};
+ my $promise = Mojo::Promise->new;
+
+ if ( my $content = $cache->get("HEAD $url") ) {
+ if ( $content eq 'n' ) {
+ return $promise->reject;
+ }
+ else {
+ return $promise->resolve($content);
+ }
+ }
+
+ $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} )
+ ->then(
+ sub {
+ my ($tx) = @_;
+ if ( $tx->result->is_success ) {
+ $cache->set( "HEAD $url", 'a' );
+ $promise->resolve('a');
+ }
+ else {
+ $cache->set( "HEAD $url", 'n' );
+ $promise->reject;
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ $cache->set( "HEAD $url", 'n' );
+ $promise->reject;
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
+sub get_wagonorder_p {
+ my ( $self, $api, $ts, $train_no ) = @_;
+ my $api_ts = $ts->strftime('%Y%m%d%H%M');
+ my $url
+ = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
+
+ my $cache = $self->{cache};
+ my $promise = Mojo::Promise->new;
+
+ if ( my $content = $cache->thaw($url) ) {
+ $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);
+ $cache->freeze( $url, $json );
+ $promise->resolve($json);
+ }
+ else {
+ my $code = $tx->code;
+ $promise->reject("HTTP ${code}");
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ my ($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->{cache};
+ my $promise = Mojo::Promise->new;
+
+ if ( my $content = $cache->thaw($url) ) {
+ return $promise->resolve($content);
+ }
+
+ $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ ->then(
+ sub {
+ my ($tx) = @_;
+
+ if ( my $err = $tx->error ) {
+ $cache->freeze( $url, {} );
+ $promise->reject("HTTP $err->{code} $err->{message}");
+ return;
+ }
+
+ my $json = $tx->result->json;
+ $cache->freeze( $url, $json );
+ $promise->resolve($json);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $cache->freeze( $url, {} );
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm
new file mode 100644
index 0000000..7671d78
--- /dev/null
+++ b/lib/Travelynx/Helper/HAFAS.pm
@@ -0,0 +1,315 @@
+package Travelynx::Helper::HAFAS;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Mojo::Promise;
+use Travel::Status::DE::HAFAS;
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_json_p {
+ my ( $self, $url, %opt ) = @_;
+
+ my $cache = $self->{main_cache};
+ my $promise = Mojo::Promise->new;
+
+ if ( $opt{realtime} ) {
+ $cache = $self->{realtime_cache};
+ }
+ $opt{encoding} //= 'ISO-8859-15';
+
+ if ( my $content = $cache->thaw($url) ) {
+ return $promise->resolve($content);
+ }
+
+ $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ ->then(
+ sub {
+ my ($tx) = @_;
+
+ if ( my $err = $tx->error ) {
+ $promise->reject(
+"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
+ );
+ return;
+ }
+
+ my $body = decode( $opt{encoding}, $tx->res->body );
+
+ $body =~ s{^TSLs[.]sls = }{};
+ $body =~ s{;$}{};
+ $body =~ s{&#x0028;}{(}g;
+ $body =~ s{&#x0029;}{)}g;
+ my $json = JSON->new->decode($body);
+ $cache->freeze( $url, $json );
+ $promise->resolve($json);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->info("hafas->get_json_p($url): $err");
+ $promise->reject("hafas->get_json_p($url): $err");
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ my $when = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now( time_zone => 'Europe/Berlin' )
+ )->subtract( minutes => $opt{lookbehind} );
+ return Travel::Status::DE::HAFAS->new_p(
+ station => $opt{eva},
+ datetime => $when,
+ lookahead => $opt{lookahead} + $opt{lookbehind},
+ results => 300,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(5),
+ );
+}
+
+sub search_location_p {
+ my ( $self, %opt ) = @_;
+
+ return Travel::Status::DE::HAFAS->new_p(
+ locationSearch => $opt{query},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(5),
+ );
+}
+
+sub get_tripid_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+
+ my $train = $opt{train};
+ my $train_desc = $train->type . ' ' . $train->train_no;
+ $train_desc =~ s{^- }{};
+
+ Travel::Status::DE::HAFAS->new_p(
+ journeyMatch => $train_desc,
+ datetime => $train->start,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10),
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my @results = $hafas->results;
+
+ if ( not @results ) {
+ $promise->reject(
+ "journeyMatch($train_desc) returned no results");
+ return;
+ }
+
+ my $result = $results[0];
+ if ( @results > 1 ) {
+ for my $journey (@results) {
+ if ( ( $journey->route )[0]->loc->name eq $train->origin ) {
+ $result = $journey;
+ last;
+ }
+ }
+ }
+
+ $promise->resolve( $result->id );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ Travel::Status::DE::HAFAS->new_p(
+ journey => {
+ id => $opt{trip_id},
+ },
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10),
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my $journey = $hafas->result;
+
+ if ($journey) {
+ $promise->resolve($journey);
+ return;
+ }
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_route_timestamps_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ Travel::Status::DE::HAFAS->new_p(
+ journey => {
+ id => $opt{trip_id},
+
+ # name => $opt{train_no},
+ },
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10),
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my $journey = $hafas->result;
+ my $ret = {};
+ my $polyline;
+
+ my $station_is_past = 1;
+ for my $stop ( $journey->route ) {
+ my $name = $stop->loc->name;
+ $ret->{$name} = $ret->{ $stop->loc->eva } = {
+ name => $stop->loc->name,
+ eva => $stop->loc->eva,
+ sched_arr => _epoch( $stop->sched_arr ),
+ sched_dep => _epoch( $stop->sched_dep ),
+ rt_arr => _epoch( $stop->rt_arr ),
+ rt_dep => _epoch( $stop->rt_dep ),
+ arr_delay => $stop->arr_delay,
+ dep_delay => $stop->dep_delay,
+ load => $stop->load
+ };
+ if ( $stop->tz_offset ) {
+ $ret->{$name}{tz_offset} = $stop->tz_offset;
+ }
+ if ( ( $stop->arr_cancelled or not $stop->sched_arr )
+ and ( $stop->dep_cancelled or not $stop->sched_dep ) )
+ {
+ $ret->{$name}{isCancelled} = 1;
+ }
+ if (
+ $station_is_past
+ and not $ret->{$name}{isCancelled}
+ and $now->epoch < (
+ $ret->{$name}{rt_arr} // $ret->{$name}{rt_dep}
+ // $ret->{$name}{sched_arr}
+ // $ret->{$name}{sched_dep} // $now->epoch
+ )
+ )
+ {
+ $station_is_past = 0;
+ }
+ $ret->{$name}{isPast} = $station_is_past;
+ }
+
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{name} ) {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat}, $coord->{eva} ] );
+ push( @station_list, $coord->{name} );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+ my $iris_stations = join( '|', $opt{train}->route );
+
+ # borders (Gr" as in "Grenze") are only returned by HAFAS.
+ # They are not stations.
+ my $hafas_stations
+ = join( '|', grep { $_ !~ m{(\(Gr\)|\)Gr)$} } @station_list );
+
+ if ( $iris_stations eq $hafas_stations
+ or index( $hafas_stations, $iris_stations ) != -1 )
+ {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->loc->eva,
+ to_eva => ( $journey->route )[-1]->loc->eva,
+ coords => \@coordinate_list,
+ };
+ }
+ else {
+ $self->{log}->debug( 'Ignoring polyline for '
+ . $opt{train}->line
+ . ": IRIS route does not agree with HAFAS route: $iris_stations != $hafas_stations"
+ );
+ }
+ }
+
+ $promise->resolve( $ret, $journey, $polyline );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm
new file mode 100644
index 0000000..deed79a
--- /dev/null
+++ b/lib/Travelynx/Helper/IRIS.pm
@@ -0,0 +1,237 @@
+package Travelynx::Helper::IRIS;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use utf8;
+
+use Mojo::Promise;
+use Mojo::UserAgent;
+use Travel::Status::DE::IRIS;
+use Travel::Status::DE::IRIS::Stations;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub get_departures {
+ my ( $self, %opt ) = @_;
+ my $station = $opt{station};
+ my $lookbehind = $opt{lookbehind} // 180;
+ my $lookahead = $opt{lookahead} // 30;
+ my $with_related = $opt{with_related} // 0;
+
+ # Berlin Hbf exists twice:
+ # - BLS / 8011160
+ # - BL / 8098160 (formerly "Berlin Hbf (tief)")
+ # Right now, travelynx assumes that station name -> EVA / DS100 is a unique
+ # map. This is not the case. Work around it here until travelynx has been
+ # adjusted properly.
+ if ( $station eq 'Berlin Hbf' or $station eq '8011160' ) {
+ $with_related = 1;
+ }
+
+ my @station_matches
+ = Travel::Status::DE::IRIS::Stations::get_station($station);
+
+ if ( @station_matches == 1 ) {
+ $station = $station_matches[0][0];
+ my $status = Travel::Status::DE::IRIS->new(
+ station => $station,
+ main_cache => $self->{main_cache},
+ realtime_cache => $self->{realtime_cache},
+ keep_transfers => 1,
+ lookbehind => 20,
+ datetime => DateTime->now( time_zone => 'Europe/Berlin' )
+ ->subtract( minutes => $lookbehind ),
+ lookahead => $lookbehind + $lookahead,
+ lwp_options => {
+ timeout => 10,
+ agent => 'travelynx/'
+ . $self->{version}
+ . ' +https://travelynx.de',
+ },
+ with_related => $with_related,
+ );
+ return {
+ results => [ $status->results ],
+ errstr => $status->errstr,
+ station_ds100 =>
+ ( $status->station ? $status->station->{ds100} : undef ),
+ station_eva =>
+ ( $status->station ? $status->station->{uic} : undef ),
+ station_name =>
+ ( $status->station ? $status->station->{name} : undef ),
+ related_stations => [ $status->related_stations ],
+ };
+ }
+ elsif ( @station_matches > 1 ) {
+ return {
+ results => [],
+ errstr =>
+ "Mehrdeutiger Stationsname: '$station'. Mögliche Eingaben: "
+ . join( q{, }, map { $_->[1] } @station_matches ),
+ };
+ }
+ else {
+ return {
+ results => [],
+ errstr => 'Unbekannte Station',
+ };
+ }
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+ my $station = $opt{station};
+ my $lookbehind = $opt{lookbehind} // 180;
+ my $lookahead = $opt{lookahead} // 30;
+ my $with_related = $opt{with_related} // 0;
+
+ # Berlin Hbf exists twice:
+ # - BLS / 8011160
+ # - BL / 8098160 (formerly "Berlin Hbf (tief)")
+ # Right now, travelynx assumes that station name -> EVA / DS100 is a unique
+ # map. This is not the case. Work around it here until travelynx has been
+ # adjusted properly.
+ if ( $station eq 'Berlin Hbf' or $station eq '8011160' ) {
+ $with_related = 1;
+ }
+
+ my @station_matches
+ = Travel::Status::DE::IRIS::Stations::get_station($station);
+
+ if ( @station_matches == 1 ) {
+ $station = $station_matches[0][0];
+ my $promise = Mojo::Promise->new;
+ Travel::Status::DE::IRIS->new_p(
+ station => $station,
+ main_cache => $self->{main_cache},
+ realtime_cache => $self->{realtime_cache},
+ keep_transfers => 1,
+ lookbehind => 20,
+ datetime => DateTime->now( time_zone => 'Europe/Berlin' )
+ ->subtract( minutes => $lookbehind ),
+ lookahead => $lookbehind + $lookahead,
+ lwp_options => {
+ timeout => 10,
+ agent => 'travelynx/'
+ . $self->{version}
+ . ' +https://travelynx.de',
+ },
+ with_related => $with_related,
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ get_station => \&Travel::Status::DE::IRIS::Stations::get_station,
+ meta => Travel::Status::DE::IRIS::Stations::get_meta(),
+ )->then(
+ sub {
+ my ($status) = @_;
+ $promise->resolve(
+ {
+ results => [ $status->results ],
+ errstr => $status->errstr,
+ station_ds100 => (
+ $status->station
+ ? $status->station->{ds100}
+ : undef
+ ),
+ station_eva => (
+ $status->station ? $status->station->{uic} : undef
+ ),
+ station_name => (
+ $status->station ? $status->station->{name} : undef
+ ),
+ related_stations => [ $status->related_stations ],
+ }
+ );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject(
+ $err,
+ {
+ results => [],
+ errstr => "Error in promise: $err",
+ }
+ );
+ return;
+ }
+ )->wait;
+ return $promise;
+ }
+ elsif ( @station_matches > 1 ) {
+ return Mojo::Promise->reject(
+ 'ambiguous station name',
+ {
+ results => [],
+ errstr => "Mehrdeutiger Stationsname: '$station'",
+ suggestions => [
+ map { { name => $_->[1], eva => $_->[2] } }
+ @station_matches
+ ],
+ }
+ );
+ }
+ else {
+ return Mojo::Promise->reject(
+ 'unknown station',
+ {
+ results => [],
+ errstr => 'Unbekannte Station',
+ }
+ );
+ }
+}
+
+sub route_diff {
+ my ( $self, $train ) = @_;
+ my @json_route;
+ my @route = $train->route;
+ my @sched_route = $train->sched_route;
+
+ my $route_idx = 0;
+ my $sched_idx = 0;
+
+ while ( $route_idx <= $#route and $sched_idx <= $#sched_route ) {
+ if ( $route[$route_idx] eq $sched_route[$sched_idx] ) {
+ push( @json_route, [ $route[$route_idx], undef, {} ] );
+ $route_idx++;
+ $sched_idx++;
+ }
+
+ # this branch is inefficient, but won't be taken frequently
+ elsif ( not( grep { $_ eq $route[$route_idx] } @sched_route ) ) {
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
+ $route_idx++;
+ }
+ else {
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
+ $sched_idx++;
+ }
+ }
+ while ( $route_idx <= $#route ) {
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
+ $route_idx++;
+ }
+ while ( $sched_idx <= $#sched_route ) {
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
+ $sched_idx++;
+ }
+ return @json_route;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/Sendmail.pm b/lib/Travelynx/Helper/Sendmail.pm
index fa3c4fd..baa1156 100644
--- a/lib/Travelynx/Helper/Sendmail.pm
+++ b/lib/Travelynx/Helper/Sendmail.pm
@@ -1,5 +1,9 @@
package Travelynx::Helper::Sendmail;
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
use strict;
use warnings;
@@ -7,7 +11,7 @@ use 5.020;
use Encode qw(encode);
use Email::Sender::Simple qw(try_to_sendmail);
-use Email::Simple;
+use MIME::Entity;
sub new {
my ( $class, %opt ) = @_;
@@ -18,14 +22,14 @@ sub new {
sub custom {
my ( $self, $to, $subject, $body ) = @_;
- my $reg_mail = Email::Simple->create(
- header => [
- To => $to,
- From => 'Travelynx <travelynx@finalrewind.org>',
- Subject => $subject,
- 'Content-Type' => 'text/plain; charset=UTF-8',
- ],
- body => encode( 'utf-8', $body ),
+ my $reg_mail = MIME::Entity->build(
+ To => $to,
+ From => $self->{config}{from},
+ Subject => encode( 'MIME-Header', $subject ),
+ Type => 'text/plain',
+ Charset => 'UTF-8',
+ Encoding => 'quoted-printable',
+ Data => encode( 'utf-8', $body ),
);
if ( $self->{config}->{disabled} ) {
@@ -38,4 +42,34 @@ sub custom {
return try_to_sendmail($reg_mail);
}
+sub age_deletion_notification {
+ my ( $self, %opt ) = @_;
+ my $name = $opt{name};
+ my $email = $opt{email};
+ my $last_seen = $opt{last_seen};
+ my $login_url = $opt{login_url};
+ my $account_url = $opt{account_url};
+ my $imprint_url = $opt{imprint_url};
+
+ my $body = "Hallo ${name},\n\n";
+ $body
+ .= "Dein travelynx-Account wurde seit dem ${last_seen} nicht verwendet.\n";
+ $body
+ .= "Im Sinne der Datensparsamkeit wird er daher in vier Wochen gelöscht.\n";
+ $body
+ .= "Falls du den Account weiterverwenden möchtest, kannst du dich unter\n";
+ $body .= "<$login_url> anmelden.\n";
+ $body
+ .= "Durch die Anmeldung wird die Löschung automatisch abgebrochen.\n\n";
+ $body
+ .= "Falls du den Account löschen, aber zuvor deine Daten exportieren möchtest,\n";
+ $body .= "kannst du dich unter obiger URL anmelden, unter <$account_url>\n";
+ $body
+ .= "deine Daten exportieren und anschließend den Account löschen lassen.\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->custom( $email,
+ 'travelynx: Löschung deines Accounts', $body );
+}
+
1;
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
new file mode 100644
index 0000000..d688004
--- /dev/null
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -0,0 +1,387 @@
+package Travelynx::Helper::Traewelling;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2023 networkException <git@nwex.de>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+use DateTime;
+use DateTime::Format::Strptime;
+use Mojo::Promise;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header} = {
+ 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx",
+ 'Accept' => 'application/json',
+ };
+ $opt{strp1} = DateTime::Format::Strptime->new(
+ pattern => '%Y-%m-%dT%H:%M:%S.000000Z',
+ time_zone => 'UTC',
+ );
+ $opt{strp2} = DateTime::Format::Strptime->new(
+ pattern => '%Y-%m-%d %H:%M:%S',
+ time_zone => 'Europe/Berlin',
+ );
+ $opt{strp3} = DateTime::Format::Strptime->new(
+ pattern => '%Y-%m-%dT%H:%M:%S%z',
+ time_zone => 'Europe/Berlin',
+ );
+
+ return bless( \%opt, $class );
+}
+
+sub epoch_to_dt_or_undef {
+ my ($epoch) = @_;
+
+ if ( not $epoch ) {
+ return undef;
+ }
+
+ return DateTime->from_epoch(
+ epoch => $epoch,
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
+ );
+}
+
+sub parse_datetime {
+ my ( $self, $dt ) = @_;
+
+ return $self->{strp1}->parse_datetime($dt)
+ // $self->{strp2}->parse_datetime($dt)
+ // $self->{strp3}->parse_datetime($dt);
+}
+
+sub get_status_p {
+ my ( $self, %opt ) = @_;
+
+ my $username = $opt{username};
+ my $token = $opt{token};
+ my $promise = Mojo::Promise->new;
+
+ my $header = {
+ 'User-Agent' => $self->{header}{'User-Agent'},
+ 'Accept' => 'application/json',
+ 'Authorization' => "Bearer $token",
+ };
+
+ $self->{user_agent}->request_timeout(20)
+ ->get_p(
+ "https://traewelling.de/api/v1/user/${username}/statuses?limit=1" =>
+ $header )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ my $err_msg
+ = "v1/user/${username}/statuses: HTTP $err->{code} $err->{message}";
+ $promise->reject( { http => $err->{code}, text => $err_msg } );
+ return;
+ }
+ else {
+ if ( my $status = $tx->result->json->{data}[0] ) {
+ my $status_id = $status->{id};
+ my $message = $status->{body};
+ my $checkin_at
+ = $self->parse_datetime( $status->{createdAt} );
+
+ my $dep_dt = $self->parse_datetime(
+ $status->{train}{origin}{departurePlanned} );
+ my $arr_dt = $self->parse_datetime(
+ $status->{train}{destination}{arrivalPlanned} );
+
+ my $dep_eva
+ = $status->{train}{origin}{evaIdentifier};
+ my $arr_eva
+ = $status->{train}{destination}{evaIdentifier};
+
+ my $dep_ds100
+ = $status->{train}{origin}{rilIdentifier};
+ my $arr_ds100
+ = $status->{train}{destination}{rilIdentifier};
+
+ my $dep_name
+ = $status->{train}{origin}{name};
+ my $arr_name
+ = $status->{train}{destination}{name};
+
+ my $category = $status->{train}{category};
+ my $linename = $status->{train}{lineName};
+ my $trip_id = $status->{train}{hafasId};
+ my ( $train_type, $train_line ) = split( qr{ }, $linename );
+ $promise->resolve(
+ {
+ http => $tx->res->code,
+ status_id => $status_id,
+ message => $message,
+ checkin => $checkin_at,
+ dep_dt => $dep_dt,
+ dep_eva => $dep_eva,
+ dep_ds100 => $dep_ds100,
+ dep_name => $dep_name,
+ arr_dt => $arr_dt,
+ arr_eva => $arr_eva,
+ arr_ds100 => $arr_ds100,
+ arr_name => $arr_name,
+ trip_id => $trip_id,
+ train_type => $train_type,
+ line => $linename,
+ line_no => $train_line,
+ category => $category,
+ }
+ );
+ return;
+ }
+ else {
+ $promise->reject(
+ { text => "v1/${username}/statuses: unknown error" } );
+ return;
+ }
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject( { text => "v1/${username}/statuses: $err" } );
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_user_p {
+ my ( $self, $uid, $token ) = @_;
+ my $ua = $self->{user_agent}->request_timeout(20);
+
+ my $header = {
+ 'User-Agent' => $self->{header}{'User-Agent'},
+ 'Accept' => 'application/json',
+ 'Authorization' => "Bearer $token",
+ };
+ my $promise = Mojo::Promise->new;
+
+ $ua->get_p( "https://traewelling.de/api/v1/auth/user" => $header )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ my $err_msg = "v1/auth/user: HTTP $err->{code} $err->{message}";
+ $promise->reject($err_msg);
+ return;
+ }
+ else {
+ my $user_data = $tx->result->json->{data};
+ $self->{model}->set_user(
+ uid => $uid,
+ trwl_id => $user_data->{id},
+ screen_name => $user_data->{displayName},
+ user_name => $user_data->{username},
+ );
+ $promise->resolve;
+ return;
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("v1/auth/user: $err");
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub logout_p {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ my $ua = $self->{user_agent}->request_timeout(20);
+
+ my $header = {
+ 'User-Agent' => $self->{header}{'User-Agent'},
+ 'Accept' => 'application/json',
+ 'Authorization' => "Bearer $token",
+ };
+ my $request = {};
+
+ $self->{model}->unlink( uid => $uid );
+
+ my $promise = Mojo::Promise->new;
+
+ $ua->post_p(
+ "https://traewelling.de/api/v1/auth/logout" => $header => json =>
+ $request )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ my $err_msg
+ = "v1/auth/logout: HTTP $err->{code} $err->{message}";
+ $promise->reject($err_msg);
+ return;
+ }
+ else {
+ $promise->resolve;
+ return;
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("v1/auth/logout: $err");
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub convert_travelynx_to_traewelling_visibility {
+ my ($travelynx_visibility) = @_;
+
+ my %visibilities = (
+
+ # public => StatusVisibility::PUBLIC
+ 100 => 0,
+
+ # travelynx => StatusVisibility::AUTHENTICATED
+ # (only visible for logged in users)
+ 80 => 4,
+
+ # followers => StatusVisibility::FOLLOWERS
+ 60 => 2,
+
+ # unlisted => StatusVisibility::PRIVATE
+ # (there is no träwelling equivalent to unlisted, their
+ # StatusVisibility::UNLISTED shows the journey on the profile)
+ 30 => 3,
+
+ # private => StatusVisibility::PRIVATE
+ 10 => 3,
+ );
+
+ return $visibilities{$travelynx_visibility};
+}
+
+sub checkin_p {
+ my ( $self, %opt ) = @_;
+
+ my $header = {
+ 'User-Agent' => $self->{header}{'User-Agent'},
+ 'Accept' => 'application/json',
+ 'Authorization' => "Bearer $opt{token}",
+ };
+
+ my $departure_ts = epoch_to_dt_or_undef( $opt{dep_ts} );
+ my $arrival_ts = epoch_to_dt_or_undef( $opt{arr_ts} );
+
+ if ($departure_ts) {
+ $departure_ts = $departure_ts->rfc3339;
+ }
+ if ($arrival_ts) {
+ $arrival_ts = $arrival_ts->rfc3339;
+ }
+
+ my $request = {
+ tripId => $opt{trip_id},
+ lineName => $opt{train_type} . ' '
+ . ( $opt{train_line} // $opt{train_no} ),
+ ibnr => \1,
+ start => q{} . $opt{dep_eva},
+ destination => q{} . $opt{arr_eva},
+ departure => $departure_ts,
+ arrival => $arrival_ts,
+ toot => $opt{data}{toot} ? \1 : \0,
+ tweet => $opt{data}{tweet} ? \1 : \0,
+ visibility =>
+ convert_travelynx_to_traewelling_visibility( $opt{visibility} )
+ };
+
+ if ( $opt{user_data}{comment} ) {
+ $request->{body} = $opt{user_data}{comment};
+ }
+
+ my $debug_prefix
+ = "v1/trains/checkin('$request->{lineName}' $request->{tripId} $request->{start} -> $request->{destination})";
+
+ my $promise = Mojo::Promise->new;
+
+ $self->{user_agent}->request_timeout(20)
+ ->post_p(
+ "https://traewelling.de/api/v1/trains/checkin" => $header => json =>
+ $request )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ my $err_msg = "HTTP $err->{code} $err->{message}";
+ if ( $tx->res->body ) {
+ if ( $err->{code} == 409 ) {
+ my $j = $tx->res->json;
+ $err_msg .= sprintf(
+': Bereits in %s eingecheckt: https://traewelling.de/status/%d',
+ $j->{message}{lineName},
+ $j->{message}{status_id}
+ );
+ }
+ else {
+ $err_msg .= ' ' . $tx->res->body;
+ }
+ }
+ $self->{log}
+ ->debug("Traewelling $debug_prefix error: $err_msg");
+ $self->{model}->log(
+ uid => $opt{uid},
+ message =>
+"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err_msg",
+ is_error => 1
+ );
+ $promise->reject( { http => $err->{code} } );
+ return;
+ }
+ $self->{log}->debug( "... success! " . $tx->res->body );
+
+ $self->{model}->log(
+ uid => $opt{uid},
+ message => "Eingecheckt in $opt{train_type} $opt{train_no}",
+ status_id => $tx->res->json->{statusId}
+ );
+ $self->{model}->set_latest_push_ts(
+ uid => $opt{uid},
+ ts => $opt{checkin_ts}
+ );
+ $promise->resolve( { http => $tx->res->code } );
+
+ # TODO store status_id in in_transit object so that it can be shown
+ # on the user status page
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("... $debug_prefix error: $err");
+ $self->{model}->log(
+ uid => $opt{uid},
+ message =>
+"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err",
+ is_error => 1
+ );
+ $promise->reject( { connection => $err } );
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm
new file mode 100644
index 0000000..69026ac
--- /dev/null
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -0,0 +1,900 @@
+package Travelynx::Model::InTransit;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use DateTime;
+use JSON;
+
+my %visibility_itoa = (
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+);
+
+my %visibility_atoi = (
+ public => 100,
+ travelynx => 80,
+ followers => 60,
+ unlisted => 30,
+ private => 10,
+);
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
+
+sub epoch_to_dt {
+ my ($epoch) = @_;
+
+ # Bugs (and user errors) may lead to undefined timestamps. Set them to
+ # 1970-01-01 to avoid crashing and show obviously wrong data instead.
+ $epoch //= 0;
+
+ return DateTime->from_epoch(
+ epoch => $epoch,
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
+ );
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+# merge [name, eva, data] from old_route into [name, undef, undef] from new_route.
+# If new_route already has eva/data, it is kept as-is.
+# changes new_route.
+sub _merge_old_route {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db};
+ my $uid = $opt{uid};
+ my $new_route = $opt{route};
+
+ my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } )
+ ->expand->hash;
+ my $old_route = $res_h ? $res_h->{route} : [];
+
+ for my $i ( 0 .. $#{$new_route} ) {
+ if ( $old_route->[$i] and $old_route->[$i][0] eq $new_route->[$i][0] ) {
+ $new_route->[$i][1] //= $old_route->[$i][1];
+ if ( not keys %{ $new_route->[$i][2] // {} } ) {
+ $new_route->[$i][2] = $old_route->[$i][2];
+ }
+ }
+ }
+
+ return $new_route;
+}
+
+sub add {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $train = $opt{train};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $checkin_station_id = $opt{departure_eva};
+ my $route = $opt{route};
+
+ my $json = JSON->new;
+
+ if ($train) {
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $train->departure_is_cancelled ? 1
+ : 0,
+ checkin_station_id => $checkin_station_id,
+ checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ dep_platform => $train->platform,
+ train_type => $train->type,
+ train_line => $train->line_no,
+ train_no => $train->train_no,
+ train_id => $train->train_id,
+ sched_departure => $train->sched_departure,
+ real_departure => $train->departure,
+ route => $json->encode($route),
+ messages => $json->encode(
+ [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
+ ),
+ data => JSON->new->encode(
+ {
+ rt => $train->departure_has_realtime ? 1
+ : 0
+ }
+ ),
+ }
+ );
+ }
+ elsif ( $journey and $stop ) {
+ my @route;
+ my $product = $journey->product_at( $stop->loc->eva )
+ // $journey->product;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->loc->name,
+ $j_stop->loc->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 ),
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => $j_stop->load
+ }
+ ]
+ );
+ if ( defined $j_stop->tz_offset ) {
+ $route[-1][2]{tz_offset} = $j_stop->tz_offset;
+ }
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stop->{dep_cancelled}
+ ? 1
+ : 0,
+ checkin_station_id => $stop->loc->eva,
+ checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ dep_platform => $stop->{platform},
+ train_type => $product->type // q{},
+ train_line => $product->line_no,
+ train_no => $product->number // q{},
+ train_id => $journey->id,
+ sched_departure => $stop->{sched_dep},
+ real_departure => $stop->{rt_dep} // $stop->{sched_dep},
+ route => $json->encode( \@route ),
+ data => JSON->new->encode( { rt => $stop->{rt_dep} ? 1 : 0 } ),
+ }
+ );
+ }
+ else {
+ die('neither train nor journey specified');
+ }
+}
+
+sub add_from_journey {
+ my ( $self, %opt ) = @_;
+
+ my $journey = $opt{journey};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ $db->insert( 'in_transit', $journey );
+}
+
+sub delete {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ $db->delete( 'in_transit', { user_id => $uid } );
+}
+
+sub delete_incomplete_checkins {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->delete( 'in_transit',
+ { checkin_time => { '<', $opt{earlier_than} } } )->rows;
+}
+
+sub postprocess {
+ my ( $self, $ret ) = @_;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $epoch = $now->epoch;
+ my @route = @{ $ret->{route} // [] };
+ my @route_after;
+ my $dep_info;
+ my $is_after = 0;
+
+ for my $station (@route) {
+ if ($is_after) {
+ push( @route_after, $station );
+ }
+ if ( $ret->{dep_name}
+ and $station->[0] eq $ret->{dep_name} )
+ {
+ $is_after = 1;
+ if ( @{$station} > 1 and not $dep_info ) {
+ $dep_info = $station->[2];
+ }
+ }
+ }
+
+ my $ts = $ret->{checkout_ts} // $ret->{checkin_ts};
+ my $action_time = epoch_to_dt($ts);
+
+ $ret->{checked_in} = !$ret->{cancelled};
+ $ret->{timestamp} = $action_time;
+ $ret->{timestamp_delta} = $now->epoch - $action_time->epoch;
+ $ret->{boarding_countdown} = -1;
+ $ret->{sched_departure} = epoch_to_dt( $ret->{sched_dep_ts} );
+ $ret->{real_departure} = epoch_to_dt( $ret->{real_dep_ts} );
+ $ret->{sched_arrival} = epoch_to_dt( $ret->{sched_arr_ts} );
+ $ret->{real_arrival} = epoch_to_dt( $ret->{real_arr_ts} );
+ $ret->{route_after} = \@route_after;
+ $ret->{extra_data} = $ret->{data};
+ $ret->{comment} = $ret->{user_data}{comment};
+
+ $ret->{visibility_str}
+ = $ret->{visibility}
+ ? $visibility_itoa{ $ret->{visibility} }
+ : 'default';
+ $ret->{effective_visibility_str}
+ = $visibility_itoa{ $ret->{effective_visibility} };
+
+ my @parsed_messages;
+ for my $message ( @{ $ret->{messages} // [] } ) {
+ my ( $ts, $msg ) = @{$message};
+ push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
+ }
+ $ret->{messages} = [ reverse @parsed_messages ];
+
+ @parsed_messages = ();
+ for my $message ( @{ $ret->{extra_data}{qos_msg} // [] } ) {
+ my ( $ts, $msg ) = @{$message};
+ push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
+ }
+ $ret->{extra_data}{qos_msg} = [@parsed_messages];
+
+ if ( $dep_info and $dep_info->{sched_arr} ) {
+ $dep_info->{sched_arr}
+ = epoch_to_dt( $dep_info->{sched_arr} );
+ $dep_info->{rt_arr} = epoch_to_dt( $dep_info->{rt_arr} );
+ $dep_info->{rt_arr_countdown} = $ret->{boarding_countdown}
+ = $dep_info->{rt_arr}->epoch - $epoch;
+ }
+
+ for my $station (@route_after) {
+ if ( @{$station} > 1 ) {
+
+ # Note: $station->[2]{sched_arr} may already have been
+ # converted to a DateTime object. This can happen when a
+ # station is present several times in a train's route, e.g.
+ # for Frankfurt Flughafen in some nightly connections.
+ my $times = $station->[2] // {};
+ if ( $times->{sched_arr}
+ and ref( $times->{sched_arr} ) ne 'DateTime' )
+ {
+ $times->{sched_arr}
+ = epoch_to_dt( $times->{sched_arr} );
+ if ( $times->{rt_arr} ) {
+ $times->{rt_arr}
+ = epoch_to_dt( $times->{rt_arr} );
+ $times->{arr_delay}
+ = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch;
+ }
+ $times->{arr} = $times->{rt_arr} || $times->{sched_arr};
+ $times->{arr_countdown} = $times->{arr}->epoch - $epoch;
+ }
+ if ( $times->{sched_dep}
+ and ref( $times->{sched_dep} ) ne 'DateTime' )
+ {
+ $times->{sched_dep}
+ = epoch_to_dt( $times->{sched_dep} );
+ if ( $times->{rt_dep} ) {
+ $times->{rt_dep}
+ = epoch_to_dt( $times->{rt_dep} );
+ $times->{dep_delay}
+ = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch;
+ }
+ $times->{dep} = $times->{rt_dep} || $times->{sched_dep};
+ $times->{dep_countdown} = $times->{dep}->epoch - $epoch;
+ }
+ }
+ }
+
+ $ret->{departure_countdown} = $ret->{real_departure}->epoch - $now->epoch;
+
+ if ( $ret->{real_arr_ts} ) {
+ $ret->{arrival_countdown} = $ret->{real_arrival}->epoch - $now->epoch;
+ $ret->{journey_duration}
+ = $ret->{real_arrival}->epoch - $ret->{real_departure}->epoch;
+ $ret->{journey_completion}
+ = $ret->{journey_duration}
+ ? 1 - ( $ret->{arrival_countdown} / $ret->{journey_duration} )
+ : 1;
+ if ( $ret->{journey_completion} > 1 ) {
+ $ret->{journey_completion} = 1;
+ }
+ elsif ( $ret->{journey_completion} < 0 ) {
+ $ret->{journey_completion} = 0;
+ }
+
+ }
+ else {
+ $ret->{arrival_countdown} = undef;
+ $ret->{journey_duration} = undef;
+ $ret->{journey_completion} = undef;
+ }
+
+ return $ret;
+}
+
+sub get {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $table = 'in_transit';
+
+ if ( $opt{with_timestamps} ) {
+ $table = 'in_transit_str';
+ }
+
+ my $res = $db->select( $table, '*', { user_id => $uid } );
+ my $ret;
+
+ if ( $opt{with_data} ) {
+ $ret = $res->expand->hash;
+ }
+ else {
+ $ret = $res->hash;
+ }
+
+ if ( $opt{with_visibility} and $ret ) {
+ $ret->{visibility_str}
+ = $ret->{visibility}
+ ? $visibility_itoa{ $ret->{visibility} }
+ : 'default';
+ $ret->{effective_visibility_str}
+ = $visibility_itoa{ $ret->{effective_visibility} };
+ }
+
+ if ( $opt{postprocess} and $ret ) {
+ return $self->postprocess($ret);
+ }
+
+ return $ret;
+}
+
+sub get_timeline {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $where = {
+ follower_id => $uid,
+ effective_visibility => { '>=', 60 }
+ };
+
+ if ( $opt{short} ) {
+ return $db->select(
+ 'follows_in_transit',
+ [
+ qw(followee_name train_type train_line train_no train_id dep_eva dep_name arr_eva arr_name)
+ ],
+ $where
+ )->hashes->each;
+ }
+
+ my $res = $db->select( 'follows_in_transit', '*', $where );
+ my $ret;
+
+ if ( $opt{with_data} ) {
+ return map { $self->postprocess($_) } $res->expand->hashes->each;
+ }
+ else {
+ return $res->hashes->each;
+ }
+}
+
+sub get_all_active {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ return $db->select( 'in_transit_str', '*', { cancelled => 0 } )
+ ->hashes->each;
+}
+
+sub get_checkout_station_id {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $status = $db->select( 'in_transit', ['checkout_station_id'],
+ { user_id => $uid } )->hash;
+
+ if ($status) {
+ return $status->{checkout_station_id};
+ }
+ return;
+}
+
+sub set_cancelled_destination {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $cancelled_destination = $opt{cancelled_destination};
+
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+
+ my $data = $res_h ? $res_h->{data} : {};
+
+ $data->{cancelled_destination} = $cancelled_destination;
+
+ $db->update(
+ 'in_transit',
+ {
+ checkout_station_id => undef,
+ checkout_time => undef,
+ arr_platform => undef,
+ sched_arrival => undef,
+ real_arrival => undef,
+ data => JSON->new->encode($data),
+ },
+ { user_id => $uid }
+ );
+}
+
+sub set_arrival {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $train = $opt{train};
+ my $route = $opt{route};
+
+ $route = $self->_merge_old_route(
+ db => $db,
+ uid => $uid,
+ route => $route
+ );
+
+ my $json = JSON->new;
+
+ $db->update(
+ 'in_transit',
+ {
+ checkout_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ arr_platform => $train->platform,
+ sched_arrival => $train->sched_arrival,
+ real_arrival => $train->arrival,
+ route => $json->encode($route),
+ messages => $json->encode(
+ [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
+ )
+ },
+ { user_id => $uid }
+ );
+}
+
+sub set_arrival_eva {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $checkout_station_id = $opt{arrival_eva};
+
+ $db->update(
+ 'in_transit',
+ {
+ checkout_station_id => $checkout_station_id,
+ },
+ { user_id => $uid }
+ );
+}
+
+sub set_arrival_times {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $sched_arr = $opt{sched_arrival};
+ my $rt_arr = $opt{rt_arrival};
+
+ $db->update(
+ 'in_transit',
+ {
+ sched_arrival => $sched_arr,
+ real_arrival => $rt_arr
+ },
+ { user_id => $uid }
+ );
+}
+
+sub set_polyline {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline = $opt{polyline};
+ my $old_id = $opt{old_id};
+
+ my $coords = $polyline->{coords};
+ my $from_eva = $polyline->{from_eva};
+ my $to_eva = $polyline->{to_eva};
+
+ my $polyline_str = JSON->new->encode($coords);
+
+ 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 {
+ eval {
+ $polyline_id = $db->insert(
+ 'polylines',
+ {
+ origin_eva => $from_eva,
+ destination_eva => $to_eva,
+ polyline => $polyline_str
+ },
+ { returning => 'id' }
+ )->hash->{id};
+ };
+ if ($@) {
+ $self->{log}->warn("add_route_timestamps: insert polyline: $@");
+ }
+ }
+ if ( $polyline_id and ( not defined $old_id or $polyline_id != $old_id ) ) {
+ $self->set_polyline_id(
+ uid => $uid,
+ db => $db,
+ polyline_id => $polyline_id
+ );
+ }
+
+}
+
+sub set_polyline_id {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline_id = $opt{polyline_id};
+
+ $db->update(
+ 'in_transit',
+ { polyline_id => $polyline_id },
+ { user_id => $uid }
+ );
+}
+
+sub set_route_data {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $route = $opt{route};
+ my $delay_msg = $opt{delay_messages};
+ my $qos_msg = $opt{qos_messages};
+ my $him_msg = $opt{him_messages};
+
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+
+ my $data = $res_h ? $res_h->{data} : {};
+
+ $data->{delay_msg} = $opt{delay_messages};
+ $data->{qos_msg} = $opt{qos_messages};
+ $data->{him_msg} = $opt{him_messages};
+
+ # no need to merge $route, it already contains HAFAS data
+ $db->update(
+ 'in_transit',
+ {
+ route => JSON->new->encode($route),
+ data => JSON->new->encode($data)
+ },
+ { user_id => $uid }
+ );
+}
+
+sub unset_arrival_data {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ $db->update(
+ 'in_transit',
+ {
+ checkout_time => undef,
+ arr_platform => undef,
+ sched_arrival => undef,
+ real_arrival => undef,
+ },
+ { user_id => $uid }
+ );
+}
+
+sub update_departure {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $train = $opt{train};
+ my $route = $opt{route};
+ my $json = JSON->new;
+
+ $route = $self->_merge_old_route(
+ db => $db,
+ uid => $uid,
+ route => $route
+ );
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ dep_platform => $train->platform,
+ real_departure => $train->departure,
+ route => $json->encode($route),
+ messages => $json->encode(
+ [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
+ ),
+ },
+ {
+ user_id => $uid,
+ train_no => $train->train_no,
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_departure_cancelled {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $train = $opt{train};
+
+ # depending on the amount of users in transit, some time may
+ # have passed between fetching $entry from the database and
+ # now. Ensure that the user is still checked into this train
+ # by selecting on uid, train no, and checkin/checkout station ID.
+ my $rows = $db->update(
+ 'in_transit',
+ {
+ cancelled => 1,
+ },
+ {
+ user_id => $uid,
+ train_no => $train->train_no,
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ )->rows;
+
+ return $rows;
+}
+
+sub update_departure_hafas {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_departure => $stop->{rt_dep},
+ },
+ {
+ user_id => $uid,
+ train_id => $journey->id,
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_arrival {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $train = $opt{train};
+ my $route = $opt{route};
+ my $json = JSON->new;
+
+ $route = $self->_merge_old_route(
+ db => $db,
+ uid => $uid,
+ route => $route
+ );
+
+ # selecting on user_id, train_no and checkout_station_id avoids a
+ # race condition when a user checks into a new train or changes
+ # their destination station while we are fetching times based on no
+ # longer valid database entries.
+ my $rows = $db->update(
+ 'in_transit',
+ {
+ arr_platform => $train->platform,
+ sched_arrival => $train->sched_arrival,
+ real_arrival => $train->arrival,
+ route => $json->encode($route),
+ messages => $json->encode(
+ [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
+ ),
+ },
+ {
+ user_id => $uid,
+ train_no => $train->train_no,
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ )->rows;
+
+ return $rows;
+}
+
+sub update_arrival_hafas {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ # TODO use old rt data if available
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->loc->name,
+ $j_stop->loc->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 ),
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => $j_stop->load
+ }
+ ]
+ );
+ if ( defined $j_stop->tz_offset ) {
+ $route[-1][2]{tz_offset} = $j_stop->tz_offset;
+ }
+ }
+
+ my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } )
+ ->expand->hash;
+ my $old_route = $res_h ? $res_h->{route} : [];
+
+ for my $i ( 0 .. $#route ) {
+ if ( $old_route->[$i] and $old_route->[$i][1] == $route[$i][1] ) {
+ for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
+ $route[$i][2]{$k} //= $old_route->[$i][2]{$k};
+ }
+ }
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_arrival => $stop->{rt_arr},
+ route => $json->encode( [@route] ),
+ },
+ {
+ user_id => $uid,
+ train_id => $journey->id,
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_data {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $new_data = $opt{data} // {};
+
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+
+ my $data = $res_h ? $res_h->{data} : {};
+
+ while ( my ( $k, $v ) = each %{$new_data} ) {
+ $data->{$k} = $v;
+ }
+
+ $db->update(
+ 'in_transit',
+ { data => JSON->new->encode($data) },
+ { user_id => $uid }
+ );
+}
+
+sub update_user_data {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $new_data = $opt{user_data} // {};
+
+ my $res_h = $db->select( 'in_transit', ['user_data'], { user_id => $uid } )
+ ->expand->hash;
+
+ my $data = $res_h ? $res_h->{user_data} : {};
+
+ while ( my ( $k, $v ) = each %{$new_data} ) {
+ $data->{$k} = $v;
+ }
+
+ $db->update(
+ 'in_transit',
+ { user_data => JSON->new->encode($data) },
+ { user_id => $uid }
+ );
+}
+
+sub update_visibility {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $visibility;
+
+ if ( $opt{visibility} and $visibility_atoi{ $opt{visibility} } ) {
+ $visibility = $visibility_atoi{ $opt{visibility} };
+ }
+
+ $db->update(
+ 'in_transit',
+ { visibility => $visibility },
+ { user_id => $uid }
+ );
+}
+
+1;
diff --git a/lib/Travelynx/Model/JourneyStatsCache.pm b/lib/Travelynx/Model/JourneyStatsCache.pm
new file mode 100755
index 0000000..d23eb04
--- /dev/null
+++ b/lib/Travelynx/Model/JourneyStatsCache.pm
@@ -0,0 +1,122 @@
+package Travelynx::Model::JourneyStatsCache;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+import JSON;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub add {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ eval {
+ $db->insert(
+ 'journey_stats',
+ {
+ user_id => $opt{uid},
+ year => $opt{year},
+ month => $opt{month},
+ data => JSON->new->encode( $opt{stats} ),
+ }
+ );
+ };
+ if ( my $err = $@ ) {
+ if ( $err =~ m{duplicate key value violates unique constraint} ) {
+
+ # If a user opens the same history page several times in
+ # short succession, there is a race condition where several
+ # Mojolicious workers execute this helper, notice that there is
+ # no up-to-date history, compute it, and insert it using the
+ # statement above. This will lead to a uniqueness violation
+ # in each successive insert. However, this is harmless, and
+ # thus ignored.
+ }
+ else {
+ # Otherwise we probably have a problem.
+ die($@);
+ }
+ }
+}
+
+sub get {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $stats = $db->select(
+ 'journey_stats',
+ ['data'],
+ {
+ user_id => $opt{uid},
+ year => $opt{year},
+ month => $opt{month}
+ }
+ )->expand->hash;
+
+ return $stats->{data};
+}
+
+# Statistics are partitioned by real_departure, which must be provided
+# when calling this function e.g. after journey deletion or editing.
+# If a joureny's real_departure has been edited, this function must be
+# called twice: once with the old and once with the new value.
+sub invalidate {
+ my ( $self, %opt ) = @_;
+
+ my $ts = $opt{ts};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ $db->delete(
+ 'journey_stats',
+ {
+ user_id => $uid,
+ year => $ts->year,
+ month => $ts->month,
+ }
+ );
+ $db->delete(
+ 'journey_stats',
+ {
+ user_id => $uid,
+ year => $ts->year,
+ month => 0,
+ }
+ );
+}
+
+sub get_yyyymm_having_stats {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $res = $db->select(
+ 'journey_stats',
+ [ 'year', 'month' ],
+ { user_id => $uid },
+ { order_by => { -asc => [ 'year', 'month' ] } }
+ );
+
+ my @ret;
+ for my $row ( $res->hashes->each ) {
+ if ( $row->{month} != 0 ) {
+ push( @ret, [ $row->{year}, $row->{month} ] );
+ }
+ }
+
+ return @ret;
+}
+
+1;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
new file mode 100755
index 0000000..97c4681
--- /dev/null
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -0,0 +1,1785 @@
+package Travelynx::Model::Journeys;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# 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 JSON;
+
+my %visibility_itoa = (
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+);
+
+my %visibility_atoi = (
+ public => 100,
+ travelynx => 80,
+ followers => 60,
+ unlisted => 30,
+ private => 10,
+);
+
+my @month_name
+ = (
+ qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
+ );
+
+sub epoch_to_dt {
+ my ($epoch) = @_;
+
+ # Bugs (and user errors) may lead to undefined timestamps. Set them to
+ # 1970-01-01 to avoid crashing and show obviously wrong data instead.
+ $epoch //= 0;
+
+ return DateTime->from_epoch(
+ epoch => $epoch,
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
+ );
+}
+
+sub min_to_human {
+ my ( $self, $minutes ) = @_;
+
+ my @ret;
+
+ if ( $minutes >= 14 * 24 * 60 ) {
+ push( @ret, int( $minutes / ( 7 * 24 * 60 ) ) . ' Wochen' );
+ }
+ elsif ( $minutes >= 7 * 24 * 60 ) {
+ push( @ret, '1 Woche' );
+ }
+ $minutes %= 7 * 24 * 60;
+
+ if ( $minutes >= 2 * 24 * 60 ) {
+ push( @ret, int( $minutes / ( 24 * 60 ) ) . ' Tage' );
+ }
+ elsif ( $minutes >= 24 * 60 ) {
+ push( @ret, '1 Tag' );
+ }
+ $minutes %= 24 * 60;
+
+ if ( $minutes >= 2 * 60 ) {
+ push( @ret, int( $minutes / 60 ) . ' Stunden' );
+ }
+ elsif ( $minutes >= 60 ) {
+ push( @ret, '1 Stunde' );
+ }
+ $minutes %= 60;
+
+ if ( $minutes >= 2 ) {
+ push( @ret, "$minutes Minuten" );
+ }
+ elsif ($minutes) {
+ push( @ret, '1 Minute' );
+ }
+
+ if ( @ret == 0 ) {
+ return '0 Minuten';
+ }
+
+ if ( @ret == 1 ) {
+ return $ret[0];
+ }
+
+ my $last = pop(@ret);
+ return join( ', ', @ret ) . " und $last";
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub stats_cache {
+ my ($self) = @_;
+ return $self->{stats_cache};
+}
+
+# Returns (journey id, error)
+# Must be called during a transaction.
+# Must perform a rollback on error.
+sub add {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db};
+ my $uid = $opt{uid};
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $dep_station = $self->{stations}->search( $opt{dep_station} );
+ my $arr_station = $self->{stations}->search( $opt{arr_station} );
+
+ if ( not $dep_station ) {
+ return ( undef, 'Unbekannter Startbahnhof' );
+ }
+ if ( not $arr_station ) {
+ return ( undef, 'Unbekannter Zielbahnhof' );
+ }
+
+ my $daily_journey_count = $db->select(
+ 'journeys_str',
+ 'count(*) as count',
+ {
+ user_id => $uid,
+ real_dep_ts => {
+ -between => [
+ $opt{rt_departure}->clone->subtract( days => 1 )->epoch,
+ $opt{rt_departure}->epoch
+ ],
+ },
+ }
+ )->hash->{count};
+
+ if ( $daily_journey_count >= 100 ) {
+ return ( undef,
+"In den 24 Stunden vor der angegebenen Abfahrtszeit wurden ${daily_journey_count} weitere Fahrten angetreten. Das kann nicht stimmen."
+ );
+ }
+
+ my $route_has_start = 0;
+ my $route_has_stop = 0;
+
+ for my $station ( @{ $opt{route} || [] } ) {
+ if ( $station eq $dep_station->{name}
+ or $station eq $dep_station->{ds100} )
+ {
+ $route_has_start = 1;
+ }
+ if ( $station eq $arr_station->{name}
+ or $station eq $arr_station->{ds100} )
+ {
+ $route_has_stop = 1;
+ }
+ }
+
+ my @route;
+
+ if ( not $route_has_start ) {
+ push( @route, [ $dep_station->{name}, $dep_station->{eva}, {} ] );
+ }
+
+ if ( $opt{route} ) {
+ my @unknown_stations;
+ for my $station ( @{ $opt{route} } ) {
+ my $station_info = $self->{stations}->search($station);
+ if ($station_info) {
+ push( @route,
+ [ $station_info->{name}, $station_info->{eva}, {} ] );
+ }
+ else {
+ push( @route, [ $station, undef, {} ] );
+ push( @unknown_stations, $station );
+ }
+ }
+
+ if ( not $opt{lax} ) {
+ if ( @unknown_stations == 1 ) {
+ return ( undef,
+ "Unbekannter Unterwegshalt: $unknown_stations[0]" );
+ }
+ elsif (@unknown_stations) {
+ return ( undef,
+ 'Unbekannte Unterwegshalte: '
+ . join( ', ', @unknown_stations ) );
+ }
+ }
+ }
+
+ if ( not $route_has_stop ) {
+ push( @route, [ $arr_station->{name}, $arr_station->{eva}, {} ] );
+ }
+
+ my $entry = {
+ user_id => $uid,
+ train_type => $opt{train_type},
+ train_line => $opt{train_line},
+ train_no => $opt{train_no},
+ train_id => 'manual',
+ checkin_station_id => $dep_station->{eva},
+ checkin_time => $now,
+ sched_departure => $opt{sched_departure},
+ real_departure => $opt{rt_departure},
+ checkout_station_id => $arr_station->{eva},
+ sched_arrival => $opt{sched_arrival},
+ real_arrival => $opt{rt_arrival},
+ checkout_time => $now,
+ edited => 0x3fff,
+ cancelled => $opt{cancelled} ? 1 : 0,
+ route => JSON->new->encode( \@route ),
+ };
+
+ if ( $opt{comment} ) {
+ $entry->{user_data}
+ = JSON->new->encode( { comment => $opt{comment} } );
+ }
+
+ my $journey_id = undef;
+ eval {
+ $journey_id
+ = $db->insert( 'journeys', $entry, { returning => 'id' } )
+ ->hash->{id};
+ $self->stats_cache->invalidate(
+ ts => $opt{rt_departure},
+ db => $db,
+ uid => $uid
+ );
+ };
+
+ if ($@) {
+ $self->{log}->error("add_journey($uid): $@");
+ return ( undef, 'add_journey failed: ' . $@ );
+ }
+
+ return ( $journey_id, undef );
+}
+
+sub add_from_in_transit {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db};
+ my $journey = $opt{journey};
+
+ delete $journey->{data};
+ $journey->{edited} = 0;
+ $journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ return $db->insert( 'journeys', $journey, { returning => 'id' } )
+ ->hash->{id};
+}
+
+sub update {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $journey_id = $opt{id};
+
+ my $rows;
+
+ my $journey = $self->get_single(
+ 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} );
+ if ( not $from_station ) {
+ die("Unbekannter Startbahnhof\n");
+ }
+ $rows = $db->update(
+ 'journeys',
+ {
+ checkin_station_id => $from_station->{eva},
+ edited => $journey->{edited} | 0x0004,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{to_name} ) {
+ my $to_station = $self->{stations}->search( $opt{to_name} );
+ if ( not $to_station ) {
+ die("Unbekannter Zielbahnhof\n");
+ }
+ $rows = $db->update(
+ 'journeys',
+ {
+ checkout_station_id => $to_station->{eva},
+ edited => $journey->{edited} | 0x0400,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{sched_departure} ) {
+ $rows = $db->update(
+ 'journeys',
+ {
+ sched_departure => $opt{sched_departure},
+ edited => $journey->{edited} | 0x0001,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{rt_departure} ) {
+ $rows = $db->update(
+ 'journeys',
+ {
+ real_departure => $opt{rt_departure},
+ edited => $journey->{edited} | 0x0002,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+
+ # stats are partitioned by rt_departure -> both the cache for
+ # the old value (see bottom of this function) and the new value
+ # (here) must be invalidated.
+ $self->stats_cache->invalidate(
+ ts => $opt{rt_departure},
+ db => $db,
+ uid => $uid,
+ );
+ }
+ if ( exists $opt{sched_arrival} ) {
+ $rows = $db->update(
+ 'journeys',
+ {
+ sched_arrival => $opt{sched_arrival},
+ edited => $journey->{edited} | 0x0100,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{rt_arrival} ) {
+ $rows = $db->update(
+ 'journeys',
+ {
+ real_arrival => $opt{rt_arrival},
+ edited => $journey->{edited} | 0x0200,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{route} ) {
+ my @new_route = map { [ $_, undef, {} ] } @{ $opt{route} };
+ $rows = $db->update(
+ 'journeys',
+ {
+ route => JSON->new->encode( \@new_route ),
+ edited => $journey->{edited} | 0x0010,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{cancelled} ) {
+ $rows = $db->update(
+ 'journeys',
+ {
+ cancelled => $opt{cancelled},
+ edited => $journey->{edited} | 0x0020,
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( exists $opt{comment} ) {
+ $journey->{user_data}{comment} = $opt{comment};
+ $rows = $db->update(
+ 'journeys',
+ {
+ user_data => JSON->new->encode( $journey->{user_data} ),
+ },
+ {
+ id => $journey_id,
+ }
+ )->rows;
+ }
+ if ( not defined $rows ) {
+ die("Invalid update key\n");
+ }
+ };
+
+ if ($@) {
+ $self->{log}->error("update($journey_id): $@");
+ return "update($journey_id): $@";
+ }
+ if ( $rows == 1 ) {
+ $self->stats_cache->invalidate(
+ ts => $journey->{rt_departure},
+ db => $db,
+ uid => $uid,
+ );
+ return undef;
+ }
+ return "update($journey_id): did not match any journey part";
+}
+
+sub delete {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $journey_id = $opt{id};
+ my $checkin_epoch = $opt{checkin};
+ my $checkout_epoch = $opt{checkout};
+
+ my @journeys = $self->get(
+ uid => $uid,
+ journey_id => $journey_id
+ );
+ if ( @journeys == 0 ) {
+ return 'Journey not found';
+ }
+ my $journey = $journeys[0];
+
+ # Double-check (comparing both ID and action epoch) to make sure we
+ # are really deleting the right journey and the user isn't just
+ # playing around with POST requests.
+ if ( $journey->{id} != $journey_id
+ or $journey->{checkin_ts} != $checkin_epoch
+ or $journey->{checkout_ts} != $checkout_epoch )
+ {
+ return 'Invalid journey data';
+ }
+
+ my $rows;
+ eval {
+ $rows = $db->delete(
+ 'journeys',
+ {
+ user_id => $uid,
+ id => $journey_id,
+ }
+ )->rows;
+ };
+
+ if ($@) {
+ $self->{log}->error("Delete($uid, $journey_id): $@");
+ return 'DELETE failed: ' . $@;
+ }
+
+ if ( $rows == 1 ) {
+ $self->stats_cache->invalidate(
+ ts => epoch_to_dt( $journey->{rt_dep_ts} ),
+ uid => $uid
+ );
+ return undef;
+ }
+ return sprintf( 'Deleted %d rows, expected 1', $rows );
+}
+
+# Used for undo (move journey entry to in_transit)
+sub pop {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db};
+ my $journey_id = $opt{journey_id};
+
+ my $journey = $db->select(
+ 'journeys',
+ '*',
+ {
+ user_id => $uid,
+ id => $journey_id
+ }
+ )->hash;
+
+ $db->delete(
+ 'journeys',
+ {
+ user_id => $uid,
+ id => $journey_id
+ }
+ );
+
+ return $journey;
+}
+
+sub get {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+
+ # If get is called from inside a transaction, db
+ # specifies the database handle performing the transaction.
+ # Otherwise, we grab a fresh one.
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my @select
+ = (
+ qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
+ );
+ my %where = (
+ user_id => $uid,
+ cancelled => 0
+ );
+ my %order = (
+ order_by => {
+ -desc => 'real_dep_ts',
+ }
+ );
+
+ if ( $opt{cancelled} ) {
+ $where{cancelled} = 1;
+ }
+
+ if ( $opt{limit} ) {
+ $order{limit} = $opt{limit};
+ }
+
+ if ( $opt{sched_dep_ts} ) {
+ $where{sched_dep_ts} = $opt{sched_dep_ts};
+ }
+
+ if ( $opt{journey_id} ) {
+ $where{journey_id} = $opt{journey_id};
+ delete $where{cancelled};
+ }
+ elsif ( $opt{after} and $opt{before} ) {
+ $where{real_dep_ts}
+ = { -between => [ $opt{after}->epoch, $opt{before}->epoch, ] };
+ }
+ elsif ( $opt{after} ) {
+ $where{real_dep_ts} = { '>=', $opt{after}->epoch };
+ }
+ elsif ( $opt{before} ) {
+ $where{real_dep_ts} = { '<=', $opt{before}->epoch };
+ }
+
+ if ( $opt{with_polyline} ) {
+ push( @select, 'polyline' );
+ }
+
+ if ( $opt{min_visibility} ) {
+ if ( $visibility_atoi{ $opt{min_visibility} } ) {
+ $opt{min_visibility} = $visibility_atoi{ $opt{min_visibility} };
+ }
+ $where{effective_visibility} = { '>=', $opt{min_visibility} };
+ }
+
+ my @travels;
+
+ my $res = $db->select( 'journeys_str', \@select, \%where, \%order );
+
+ for my $entry ( $res->expand->hashes->each ) {
+
+ my $ref = {
+ id => $entry->{journey_id},
+ type => $entry->{train_type},
+ line => $entry->{train_line},
+ no => $entry->{train_no},
+ from_eva => $entry->{dep_eva},
+ from_ds100 => $entry->{dep_ds100},
+ from_name => $entry->{dep_name},
+ from_latlon => [ $entry->{dep_lat}, $entry->{dep_lon} ],
+ checkin_ts => $entry->{checkin_ts},
+ sched_dep_ts => $entry->{sched_dep_ts},
+ rt_dep_ts => $entry->{real_dep_ts},
+ to_eva => $entry->{arr_eva},
+ to_ds100 => $entry->{arr_ds100},
+ to_name => $entry->{arr_name},
+ to_latlon => [ $entry->{arr_lat}, $entry->{arr_lon} ],
+ checkout_ts => $entry->{checkout_ts},
+ sched_arr_ts => $entry->{sched_arr_ts},
+ rt_arr_ts => $entry->{real_arr_ts},
+ messages => $entry->{messages},
+ route => $entry->{route},
+ edited => $entry->{edited},
+ user_data => $entry->{user_data},
+ visibility => $entry->{visibility},
+ effective_visibility => $entry->{effective_visibility},
+ };
+
+ if ( $opt{with_visibility} ) {
+ $ref->{visibility_str}
+ = $ref->{visibility}
+ ? $visibility_itoa{ $ref->{visibility} }
+ : 'default';
+ $ref->{effective_visibility_str}
+ = $visibility_itoa{ $ref->{effective_visibility} };
+ }
+
+ if ( $opt{with_polyline} ) {
+ $ref->{polyline} = $entry->{polyline};
+ }
+
+ if ( $opt{with_datetime} ) {
+ $ref->{checkin} = epoch_to_dt( $ref->{checkin_ts} );
+ $ref->{sched_departure}
+ = epoch_to_dt( $ref->{sched_dep_ts} );
+ $ref->{rt_departure} = epoch_to_dt( $ref->{rt_dep_ts} );
+ $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} );
+ 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} );
+ }
+ }
+ }
+ }
+
+ if ( $opt{verbose} ) {
+ my $rename = $self->{renamed_station};
+ for my $stop ( @{ $ref->{route} } ) {
+ if ( $stop->[0] =~ m{^Betriebsstelle nicht bekannt (\d+)$} ) {
+ if ( my $s = $self->{stations}->get_by_eva($1) ) {
+ $stop->[0] = $s->{name};
+ }
+ }
+ if ( $rename->{ $stop->[0] } ) {
+ $stop->[0] = $rename->{ $stop->[0] };
+ }
+ }
+ $ref->{cancelled} = $entry->{cancelled};
+ my @parsed_messages;
+ for my $message ( @{ $ref->{messages} // [] } ) {
+ my ( $ts, $msg ) = @{$message};
+ push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
+ }
+ $ref->{messages} = [ reverse @parsed_messages ];
+ $ref->{sched_duration}
+ = defined $ref->{sched_arr_ts}
+ ? $ref->{sched_arr_ts} - $ref->{sched_dep_ts}
+ : undef;
+ $ref->{rt_duration}
+ = defined $ref->{rt_arr_ts}
+ ? $ref->{rt_arr_ts} - $ref->{rt_dep_ts}
+ : undef;
+ my ( $km_polyline, $km_route, $km_beeline, $skip )
+ = $self->get_travel_distance($ref);
+ $ref->{km_route} = $km_polyline || $km_route;
+ $ref->{skip_route} = $km_polyline ? 0 : $skip;
+ $ref->{km_beeline} = $km_beeline;
+ $ref->{skip_beeline} = $skip;
+ my $kmh_divisor
+ = ( $ref->{rt_duration} // $ref->{sched_duration} // 999999 )
+ / 3600;
+ $ref->{kmh_route}
+ = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1;
+ $ref->{kmh_beeline}
+ = $kmh_divisor
+ ? $ref->{km_beeline} / $kmh_divisor
+ : -1;
+ }
+
+ push( @travels, $ref );
+ }
+
+ return @travels;
+}
+
+sub get_single {
+ my ( $self, %opt ) = @_;
+
+ $opt{cancelled} = 'any';
+ my @journeys = $self->get(%opt);
+ if ( @journeys == 0 ) {
+ return undef;
+ }
+
+ return $journeys[0];
+}
+
+sub get_latest {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $latest_successful = $db->select(
+ 'journeys_str',
+ '*',
+ {
+ user_id => $uid,
+ cancelled => 0
+ },
+ {
+ order_by => { -desc => 'real_dep_ts' },
+ limit => 1
+ }
+ )->expand->hash;
+
+ if ($latest_successful) {
+ $latest_successful->{visibility_str}
+ = $latest_successful->{visibility}
+ ? $visibility_itoa{ $latest_successful->{visibility} }
+ : 'default';
+ $latest_successful->{effective_visibility_str}
+ = $visibility_itoa{ $latest_successful->{effective_visibility} };
+ }
+
+ my $latest = $db->select(
+ 'journeys_str',
+ '*',
+ {
+ user_id => $uid,
+ },
+ {
+ order_by => { -desc => 'journey_id' },
+ limit => 1
+ }
+ )->expand->hash;
+
+ if ($latest) {
+ $latest->{visibility_str}
+ = $latest->{visibility}
+ ? $visibility_itoa{ $latest->{visibility} }
+ : 'default';
+ $latest->{effective_visibility_str}
+ = $visibility_itoa{ $latest->{effective_visibility} };
+ }
+
+ return ( $latest_successful, $latest );
+}
+
+sub get_oldest_ts {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h = $db->select(
+ 'journeys_str',
+ ['sched_dep_ts'],
+ {
+ user_id => $uid,
+ },
+ {
+ limit => 1,
+ order_by => {
+ -asc => 'real_dep_ts',
+ },
+ }
+ )->hash;
+
+ if ($res_h) {
+ return epoch_to_dt( $res_h->{sched_dep_ts} );
+ }
+ return undef;
+}
+
+sub get_latest_checkout_station_id {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h = $db->select(
+ 'journeys',
+ ['checkout_station_id'],
+ {
+ user_id => $uid,
+ cancelled => 0
+ },
+ {
+ limit => 1,
+ order_by => { -desc => 'real_departure' }
+ }
+ )->hash;
+
+ if ( not $res_h ) {
+ return;
+ }
+
+ return $res_h->{checkout_station_id};
+}
+
+sub get_latest_checkout_stations {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $limit = $opt{limit} // 5;
+
+ my $res = $db->select(
+ 'journeys_str',
+ [ 'arr_name', 'arr_eva', 'train_id' ],
+ {
+ user_id => $uid,
+ cancelled => 0
+ },
+ {
+ limit => $limit,
+ order_by => { -desc => 'journey_id' }
+ }
+ );
+
+ if ( not $res ) {
+ return;
+ }
+
+ my @ret;
+
+ while ( my $row = $res->hash ) {
+ push(
+ @ret,
+ {
+ name => $row->{arr_name},
+ eva => $row->{arr_eva},
+ hafas => ( $row->{train_id} =~ m{[|]} ? 1 : 0 ),
+ }
+ );
+ }
+
+ return @ret;
+}
+
+sub get_nav_years {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res = $db->select(
+ 'journeys',
+ 'distinct extract(year from real_departure) as year',
+ { user_id => $uid },
+ { order_by => { -asc => 'year' } }
+ );
+
+ my @ret;
+ for my $row ( $res->hashes->each ) {
+ push( @ret, [ $row->{year}, $row->{year} ] );
+ }
+ return @ret;
+}
+
+sub get_years {
+ my ( $self, %opt ) = @_;
+
+ my @years = $self->get_nav_years(%opt);
+
+ for my $year (@years) {
+ my $stats = $self->stats_cache->get(
+ uid => $opt{uid},
+ year => $year,
+ month => 0,
+ );
+ $year->[2] = $stats // {};
+ }
+ return @years;
+}
+
+sub get_months_for_year {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $year = $opt{year};
+
+ my $res = $db->select(
+ 'journeys',
+'distinct extract(year from real_departure) as year, extract(month from real_departure) as month',
+ { user_id => $uid },
+ { order_by => { -asc => 'year' } }
+ );
+
+ my @ret;
+
+ for my $month ( 1 .. 12 ) {
+ push( @ret,
+ [ sprintf( '%d/%02d', $year, $month ), $month_name[ $month - 1 ] ]
+ );
+ }
+
+ for my $row ( $res->hashes->each ) {
+ if ( $row->{year} == $year ) {
+
+ my $stats = $self->stats_cache->get(
+ db => $db,
+ uid => $uid,
+ year => $year,
+ month => $row->{month}
+ );
+
+ # undef -> no journeys for this month; empty hash -> no cached stats
+ $ret[ $row->{month} - 1 ][2] = $stats // {};
+ }
+ }
+ return @ret;
+}
+
+sub get_yyyymm_having_journeys {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $res = $db->select(
+ 'journeys',
+ "distinct to_char(real_departure, 'YYYY.MM') as yearmonth",
+ { user_id => $uid },
+ { order_by => { -asc => 'yearmonth' } }
+ );
+
+ my @ret;
+ for my $row ( $res->hashes->each ) {
+ push( @ret, [ split( qr{[.]}, $row->{yearmonth} ) ] );
+ }
+
+ return @ret;
+}
+
+sub generate_missing_stats {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my @journey_months = $self->get_yyyymm_having_journeys(
+ uid => $uid,
+ db => $db
+ );
+ my @stats_months = $self->stats_cache->get_yyyymm_having_stats(
+ uid => $uid,
+ $db => $db
+ );
+
+ my $stats_index = 0;
+
+ for my $journey_index ( 0 .. $#journey_months ) {
+ if ( $stats_index < @stats_months
+ and $journey_months[$journey_index][0]
+ == $stats_months[$stats_index][0]
+ and $journey_months[$journey_index][1]
+ == $stats_months[$stats_index][1] )
+ {
+ $stats_index++;
+ }
+ else {
+ my ( $year, $month ) = @{ $journey_months[$journey_index] };
+ $self->get_stats(
+ uid => $uid,
+ db => $db,
+ year => $year,
+ month => $month,
+ write_only => 1
+ );
+ }
+ }
+}
+
+sub get_nav_months {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $filter_year = $opt{year};
+ my $filter_month = $opt{month};
+
+ my $selected_index = undef;
+
+ my $res = $db->select(
+ 'journeys',
+ "distinct to_char(real_departure, 'YYYY.MM') as yearmonth",
+ { user_id => $uid },
+ { order_by => { -asc => 'yearmonth' } }
+ );
+
+ my @months;
+ for my $row ( $res->hashes->each ) {
+ my ( $year, $month ) = split( qr{[.]}, $row->{yearmonth} );
+ push( @months, [ $year, $month ] );
+ if ( $year eq $filter_year and $month eq $filter_month ) {
+ $selected_index = $#months;
+ }
+ }
+
+ # returns (previous entry, current month, next entry). if there is no
+ # previous or next entry, the corresponding field is undef. Previous/next
+ # entry is usually previous/next month, but may also have a distance of
+ # more than one month if there are months without travels
+ my @ret = ( undef, undef, undef );
+
+ $ret[1] = [
+ "${filter_year}/${filter_month}",
+ $month_name[ $filter_month - 1 ] // $filter_month
+ ];
+
+ if ( not defined $selected_index ) {
+ return @ret;
+ }
+
+ if ( $selected_index > 0 and $months[ $selected_index - 1 ] ) {
+ my ( $year, $month ) = @{ $months[ $selected_index - 1 ] };
+ $ret[0] = [ "${year}/${month}", "${month}.${year}" ];
+ }
+ if ( $selected_index < $#months ) {
+ my ( $year, $month ) = @{ $months[ $selected_index + 1 ] };
+ $ret[2] = [ "${year}/${month}", "${month}.${year}" ];
+ }
+
+ return @ret;
+}
+
+sub sanity_check {
+ my ( $self, $journey, $lax ) = @_;
+
+ if ( defined $journey->{sched_duration}
+ and $journey->{sched_duration} <= 0 )
+ {
+ return
+'Die geplante Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+ }
+ if ( defined $journey->{rt_duration}
+ and $journey->{rt_duration} <= 0 )
+ {
+ return
+'Die Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+ }
+ if ( $journey->{sched_duration}
+ and $journey->{sched_duration} > 60 * 60 * 24 )
+ {
+ return 'Die Fahrt ist länger als 24 Stunden.';
+ }
+ if ( $journey->{rt_duration}
+ and $journey->{rt_duration} > 60 * 60 * 24 )
+ {
+ return 'Die Fahrt ist länger als 24 Stunden.';
+ }
+ if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 ) {
+ return 'Fahrten mit über 500 km/h? Schön wär\'s.';
+ }
+ if ( $journey->{route} and @{ $journey->{route} } > 199 ) {
+ my $stop_count = @{ $journey->{route} };
+ return
+"Die Fahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
+ }
+ if ( $journey->{edited} & 0x0010 and not $lax ) {
+ my @unknown_stations
+ = $self->{stations}
+ ->grep_unknown( map { $_->[0] } @{ $journey->{route} } );
+ if (@unknown_stations) {
+ return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );
+ }
+ }
+
+ return undef;
+}
+
+sub get_travel_distance {
+ my ( $self, $journey ) = @_;
+
+ my $from = $journey->{from_name};
+ my $from_eva = $journey->{from_eva};
+ my $from_latlon = $journey->{from_latlon};
+ my $to = $journey->{to_name};
+ my $to_eva = $journey->{to_eva};
+ my $to_latlon = $journey->{to_latlon};
+ my $route_ref = $journey->{route};
+ my $polyline_ref = $journey->{polyline};
+
+ if ( not $to ) {
+ $self->{log}
+ ->warn("Journey $journey->{id} has no to_name for EVA $to_eva");
+ }
+
+ if ( not $from ) {
+ $self->{log}
+ ->warn("Journey $journey->{id} has no from_name for EVA $from_eva");
+ }
+
+ my $distance_polyline = 0;
+ my $distance_intermediate = 0;
+ my $distance_beeline = 0;
+ my $skipped = 0;
+ my $geo = GIS::Distance->new();
+ my @stations = map { $_->[0] } @{$route_ref};
+ my @route = after_incl { $_ eq $from } @stations;
+ @route = before_incl { $_ eq $to } @route;
+
+ if ( @route < 2 ) {
+
+ # I AM ERROR
+ return ( 0, 0, 0 );
+ }
+
+ my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva }
+ @{ $polyline_ref // [] };
+ @polyline
+ = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline;
+
+ my $prev_station = shift @polyline;
+ for my $station (@polyline) {
+ $distance_polyline += $geo->distance_metal(
+ $prev_station->[1], $prev_station->[0],
+ $station->[1], $station->[0]
+ );
+ $prev_station = $station;
+ }
+
+ $prev_station = $self->{latlon_by_station}->{ shift @route };
+ if ( not $prev_station ) {
+ return ( $distance_polyline, 0, 0 );
+ }
+
+ for my $station_name (@route) {
+ if ( my $station = $self->{latlon_by_station}->{$station_name} ) {
+ $distance_intermediate += $geo->distance_metal(
+ $prev_station->[0], $prev_station->[1],
+ $station->[0], $station->[1]
+ );
+ $prev_station = $station;
+ }
+ }
+
+ $distance_beeline = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} );
+
+ return ( $distance_polyline, $distance_intermediate,
+ $distance_beeline, $skipped );
+}
+
+sub grep_single {
+ my ( $self, @journeys ) = @_;
+
+ my %num_by_trip;
+ for my $journey (@journeys) {
+ if ( $journey->{from_name} and $journey->{to_name} ) {
+ $num_by_trip{ $journey->{from_name} . '|' . $journey->{to_name} }
+ += 1;
+ }
+ }
+
+ return
+ grep { $num_by_trip{ $_->{from_name} . '|' . $_->{to_name} } == 1 }
+ @journeys;
+}
+
+sub compute_review {
+ my ( $self, $stats, @journeys ) = @_;
+ my $longest_km;
+ my $longest_t;
+ my $shortest_km;
+ my $shortest_t;
+ my $most_delayed;
+ my $most_delay;
+ my $most_undelay;
+ my $num_cancelled = 0;
+ my $num_fgr = 0;
+ my $num_punctual = 0;
+ my $message_count = 0;
+ my %num_by_message;
+ my %num_by_wrtype;
+ my %num_by_linetype;
+ my %num_by_stop;
+ my %num_by_trip;
+
+ if ( not $stats or not @journeys or $stats->{num_trains} == 0 ) {
+ return;
+ }
+
+ my %review;
+
+ for my $journey (@journeys) {
+ if ( $journey->{cancelled} ) {
+ $num_cancelled += 1;
+ next;
+ }
+
+ my %seen;
+
+ if ( $journey->{rt_duration} and $journey->{rt_duration} > 0 ) {
+ if ( not $longest_t
+ or $journey->{rt_duration} > $longest_t->{rt_duration} )
+ {
+ $longest_t = $journey;
+ }
+ if ( not $shortest_t
+ or $journey->{rt_duration} < $shortest_t->{rt_duration} )
+ {
+ $shortest_t = $journey;
+ }
+ }
+
+ if ( $journey->{km_route} ) {
+ if ( not $longest_km
+ or $journey->{km_route} > $longest_km->{km_route} )
+ {
+ $longest_km = $journey;
+ }
+ if ( not $shortest_km
+ or $journey->{km_route} < $shortest_km->{km_route} )
+ {
+ $shortest_km = $journey;
+ }
+ }
+
+ if ( $journey->{messages} and @{ $journey->{messages} } ) {
+ $message_count += 1;
+ for my $message ( @{ $journey->{messages} } ) {
+ if ( not $seen{ $message->[1] } ) {
+ $num_by_message{ $message->[1] } += 1;
+ $seen{ $message->[1] } = 1;
+ }
+ }
+ }
+
+ if ( $journey->{type} ) {
+ $num_by_linetype{ $journey->{type} } += 1;
+ }
+
+ if ( $journey->{from_name} ) {
+ $num_by_stop{ $journey->{from_name} } += 1;
+ }
+ if ( $journey->{to_name} ) {
+ $num_by_stop{ $journey->{to_name} } += 1;
+ }
+ if ( $journey->{from_name} and $journey->{to_name} ) {
+ $num_by_trip{ $journey->{from_name} . '|' . $journey->{to_name} }
+ += 1;
+ }
+
+ if ( $journey->{sched_dep_ts} and $journey->{rt_dep_ts} ) {
+ $journey->{delay_dep}
+ = ( $journey->{rt_dep_ts} - $journey->{sched_dep_ts} ) / 60;
+ }
+ if ( $journey->{sched_arr_ts} and $journey->{rt_arr_ts} ) {
+ $journey->{delay_arr}
+ = ( $journey->{rt_arr_ts} - $journey->{sched_arr_ts} ) / 60;
+ }
+
+ if ( $journey->{delay_arr} and $journey->{delay_arr} >= 60 ) {
+ $num_fgr += 1;
+ }
+ if ( not $journey->{delay_arr} and not $journey->{delay_dep} ) {
+ $num_punctual += 1;
+ }
+
+ if ( $journey->{delay_arr} and $journey->{delay_arr} > 0 ) {
+ if ( not $most_delayed
+ or $journey->{delay_arr} > $most_delayed->{delay_arr} )
+ {
+ $most_delayed = $journey;
+ }
+ }
+
+ if ( $journey->{rt_duration}
+ and $journey->{sched_duration}
+ and $journey->{rt_duration} > 0
+ and $journey->{sched_duration} > 0 )
+ {
+ my $slowdown = $journey->{rt_duration} - $journey->{sched_duration};
+ my $speedup = -$slowdown;
+ if (
+ not $most_delay
+ or $slowdown > (
+ $most_delay->{rt_duration} - $most_delay->{sched_duration}
+ )
+ )
+ {
+ $most_delay = $journey;
+ }
+ if (
+ not $most_undelay
+ or $speedup > (
+ $most_undelay->{sched_duration}
+ - $most_undelay->{rt_duration}
+ )
+ )
+ {
+ $most_undelay = $journey;
+ }
+ }
+ }
+
+ my @linetypes = sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $num_by_linetype{$_} ] } keys %num_by_linetype;
+ my @stops = sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $num_by_stop{$_} ] } keys %num_by_stop;
+ my @trips = sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $num_by_trip{$_} ] } keys %num_by_trip;
+
+ my @reasons = sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $num_by_message{$_} ] } keys %num_by_message;
+
+ $review{num_stops} = scalar @stops;
+ $review{km_circle} = $stats->{km_route} / 40030;
+ $review{km_diag} = $stats->{km_route} / 12742;
+
+ $review{trains_per_day} = sprintf( '%.1f', $stats->{num_trains} / 365 );
+ $review{km_route} = sprintf( '%.0f', $stats->{km_route} );
+ $review{km_beeline} = sprintf( '%.0f', $stats->{km_beeline} );
+ $review{km_circle_h} = sprintf( '%.1f', $review{km_circle} );
+ $review{km_diag_h} = sprintf( '%.1f', $review{km_diag} );
+
+ $review{trains_per_day} =~ tr{.}{,};
+ $review{km_circle_h} =~ tr{.}{,};
+ $review{km_diag_h} =~ tr{.}{,};
+
+ my $min_total = $stats->{min_travel_real} + $stats->{min_interchange_real};
+ $review{traveling_min_total} = $min_total;
+ $review{traveling_percentage_year}
+ = sprintf( "%.1f%%", $min_total * 100 / 525948.77 );
+ $review{traveling_percentage_year} =~ tr{.}{,};
+ $review{traveling_time_year} = $self->min_to_human($min_total);
+
+ if (@linetypes) {
+ $review{typical_type_1} = $linetypes[0][0];
+ }
+ if ( @linetypes > 1 ) {
+ $review{typical_type_2} = $linetypes[1][0];
+ }
+ if ( @stops >= 3 ) {
+ my $desc = q{};
+ $review{typical_stops_3} = [ $stops[0][0], $stops[1][0], $stops[2][0] ];
+ }
+ elsif ( @stops == 2 ) {
+ $review{typical_stops_2} = [ $stops[0][0], $stops[1][0] ];
+ }
+ $review{typical_time}
+ = $self->min_to_human( $stats->{min_travel_real} / $stats->{num_trains} );
+ $review{typical_km}
+ = sprintf( '%.0f', $stats->{km_route} / $stats->{num_trains} );
+ $review{typical_kmh} = sprintf( '%.0f',
+ $stats->{km_route} / ( $stats->{min_travel_real} / 60 ) );
+ $review{typical_delay_dep}
+ = sprintf( '%.0f', $stats->{delay_dep} / $stats->{num_trains} );
+ $review{typical_delay_dep_h}
+ = $self->min_to_human( $review{typical_delay_dep} );
+ $review{typical_delay_arr}
+ = sprintf( '%.0f', $stats->{delay_arr} / $stats->{num_trains} );
+ $review{typical_delay_arr_h}
+ = $self->min_to_human( $review{typical_delay_arr} );
+
+ if ($longest_t) {
+ $review{longest_t_time}
+ = $self->min_to_human( $longest_t->{rt_duration} / 60 );
+ $review{longest_t_type} = $longest_t->{type};
+ $review{longest_t_lineno} = $longest_t->{line} // $longest_t->{no};
+ $review{longest_t_from} = $longest_t->{from_name};
+ $review{longest_t_to} = $longest_t->{to_name};
+ $review{longest_t_id} = $longest_t->{id};
+ }
+
+ if ($longest_km) {
+ $review{longest_km_km} = sprintf( '%.0f', $longest_km->{km_route} );
+ $review{longest_km_type} = $longest_km->{type};
+ $review{longest_km_lineno} = $longest_km->{line} // $longest_km->{no};
+ $review{longest_km_from} = $longest_km->{from_name};
+ $review{longest_km_to} = $longest_km->{to_name};
+ $review{longest_km_id} = $longest_km->{id};
+ }
+
+ if ($shortest_t) {
+ $review{shortest_t_time}
+ = $self->min_to_human( $shortest_t->{rt_duration} / 60 );
+ $review{shortest_t_type} = $shortest_t->{type};
+ $review{shortest_t_lineno} = $shortest_t->{line} // $shortest_t->{no};
+ $review{shortest_t_from} = $shortest_t->{from_name};
+ $review{shortest_t_to} = $shortest_t->{to_name};
+ $review{shortest_t_id} = $shortest_t->{id};
+ }
+
+ if ($shortest_km) {
+ $review{shortest_km_m}
+ = sprintf( '%.0f', $shortest_km->{km_route} * 1000 );
+ $review{shortest_km_type} = $shortest_km->{type};
+ $review{shortest_km_lineno} = $shortest_km->{line}
+ // $shortest_km->{no};
+ $review{shortest_km_from} = $shortest_km->{from_name};
+ $review{shortest_km_to} = $shortest_km->{to_name};
+ $review{shortest_km_id} = $shortest_km->{id};
+ }
+
+ if ($most_delayed) {
+ $review{most_delayed_type} = $most_delayed->{type};
+ $review{most_delayed_delay_dep}
+ = $self->min_to_human( $most_delayed->{delay_dep} );
+ $review{most_delayed_delay_arr}
+ = $self->min_to_human( $most_delayed->{delay_arr} );
+ $review{most_delayed_lineno} = $most_delayed->{line}
+ // $most_delayed->{no};
+ $review{most_delayed_from} = $most_delayed->{from_name};
+ $review{most_delayed_to} = $most_delayed->{to_name};
+ $review{most_delayed_id} = $most_delayed->{id};
+ }
+
+ if ($most_delay) {
+ $review{most_delay_type} = $most_delay->{type};
+ $review{most_delay_delay_dep} = $most_delay->{delay_dep};
+ $review{most_delay_delay_arr} = $most_delay->{delay_arr};
+ $review{most_delay_sched_time}
+ = $self->min_to_human( $most_delay->{sched_duration} / 60 );
+ $review{most_delay_real_time}
+ = $self->min_to_human( $most_delay->{rt_duration} / 60 );
+ $review{most_delay_delta}
+ = $self->min_to_human(
+ ( $most_delay->{rt_duration} - $most_delay->{sched_duration} )
+ / 60 );
+ $review{most_delay_lineno} = $most_delay->{line} // $most_delay->{no};
+ $review{most_delay_from} = $most_delay->{from_name};
+ $review{most_delay_to} = $most_delay->{to_name};
+ $review{most_delay_id} = $most_delay->{id};
+ }
+
+ if ($most_undelay) {
+ $review{most_undelay_type} = $most_undelay->{type};
+ $review{most_undelay_delay_dep} = $most_undelay->{delay_dep};
+ $review{most_undelay_delay_arr} = $most_undelay->{delay_arr};
+ $review{most_undelay_sched_time}
+ = $self->min_to_human( $most_undelay->{sched_duration} / 60 );
+ $review{most_undelay_real_time}
+ = $self->min_to_human( $most_undelay->{rt_duration} / 60 );
+ $review{most_undelay_delta}
+ = $self->min_to_human(
+ ( $most_undelay->{sched_duration} - $most_undelay->{rt_duration} )
+ / 60 );
+ $review{most_undelay_lineno} = $most_undelay->{line}
+ // $most_undelay->{no};
+ $review{most_undelay_from} = $most_undelay->{from_name};
+ $review{most_undelay_to} = $most_undelay->{to_name};
+ $review{most_undelay_id} = $most_undelay->{id};
+ }
+
+ $review{issue_percent}
+ = sprintf( '%.0f%%', $message_count * 100 / $stats->{num_trains} );
+ for my $i ( 0 .. 2 ) {
+ if ( $reasons[$i] ) {
+ my $p = 'issue' . ( $i + 1 );
+ $review{"${p}_count"} = $reasons[$i][1];
+ $review{"${p}_text"} = $reasons[$i][0];
+ }
+ }
+
+ $review{cancel_count} = $num_cancelled;
+ $review{fgr_percent} = $num_fgr * 100 / $stats->{num_trains};
+ $review{fgr_percent_h} = sprintf( '%.1f%%', $review{fgr_percent} );
+ $review{fgr_percent_h} =~ tr{.}{,};
+ $review{punctual_percent} = $num_punctual * 100 / $stats->{num_trains};
+ $review{punctual_percent_h}
+ = sprintf( '%.1f%%', $review{punctual_percent} );
+ $review{punctual_percent_h} =~ tr{.}{,};
+
+ my $top_trip_count = 0;
+ my $single_trip_count = 0;
+ for my $i ( 0 .. 3 ) {
+ if ( $trips[$i] ) {
+ my ( $from, $to ) = split( qr{[|]}, $trips[$i][0] );
+ my $found = 0;
+ for my $j ( 0 .. $#{ $review{top_trips} } ) {
+ if ( $review{top_trips}[$j][0] eq $to
+ and $review{top_trips}[$j][2] eq $from )
+ {
+ $review{top_trips}[$j][1] = '↔';
+ $found = 1;
+ last;
+ }
+ }
+ if ( not $found ) {
+ push( @{ $review{top_trips} }, [ $from, '→', $to ] );
+ }
+ $top_trip_count += $trips[$i][1];
+ }
+ }
+
+ for my $trip (@trips) {
+ if ( $trip->[1] == 1 ) {
+ $single_trip_count += 1;
+ if ( @{ $review{single_trips} // [] } < 3 ) {
+ push(
+ @{ $review{single_trips} },
+ [ split( qr{[|]}, $trip->[0] ) ]
+ );
+ }
+ }
+ }
+
+ $review{top_trip_count} = $top_trip_count;
+ $review{top_trip_percent_h}
+ = sprintf( '%.1f%%', $top_trip_count * 100 / $stats->{num_trains} );
+ $review{top_trip_percent_h} =~ tr{.}{,};
+
+ $review{single_trip_count} = $single_trip_count;
+ $review{single_trip_percent_h}
+ = sprintf( '%.1f%%', $single_trip_count * 100 / $stats->{num_trains} );
+ $review{single_trip_percent_h} =~ tr{.}{,};
+
+ return \%review;
+}
+
+sub compute_stats {
+ my ( $self, @journeys ) = @_;
+ my $km_route = 0;
+ my $km_beeline = 0;
+ my $min_travel_sched = 0;
+ my $min_travel_real = 0;
+ my $delay_dep = 0;
+ my $delay_arr = 0;
+ my $interchange_real = 0;
+ my $num_trains = 0;
+ my $num_journeys = 0;
+ my @inconsistencies;
+
+ my $next_departure = 0;
+ my $next_id;
+ my $next_train;
+
+ for my $journey (@journeys) {
+ $num_trains++;
+ $km_route += $journey->{km_route};
+ $km_beeline += $journey->{km_beeline};
+ if ( $journey->{sched_duration}
+ and $journey->{sched_duration} > 0 )
+ {
+ $min_travel_sched += $journey->{sched_duration} / 60;
+ }
+ if ( $journey->{rt_duration} and $journey->{rt_duration} > 0 ) {
+ $min_travel_real += $journey->{rt_duration} / 60;
+ }
+ if ( $journey->{sched_dep_ts} and $journey->{rt_dep_ts} ) {
+ $delay_dep
+ += ( $journey->{rt_dep_ts} - $journey->{sched_dep_ts} ) / 60;
+ }
+ if ( $journey->{sched_arr_ts} and $journey->{rt_arr_ts} ) {
+ $delay_arr
+ += ( $journey->{rt_arr_ts} - $journey->{sched_arr_ts} ) / 60;
+ }
+
+ # Note that journeys are sorted from recent to older entries
+ if ( $journey->{rt_arr_ts}
+ and $next_departure
+ and $next_departure - $journey->{rt_arr_ts} < ( 60 * 60 ) )
+ {
+ if ( $next_departure - $journey->{rt_arr_ts} < 0 ) {
+ push(
+ @inconsistencies,
+ {
+ conflict => {
+ train => $journey->{type} . ' '
+ . ( $journey->{line} // $journey->{no} ),
+ arr => epoch_to_dt( $journey->{rt_arr_ts} )
+ ->strftime('%d.%m.%Y %H:%M'),
+ id => $journey->{id},
+ },
+ ignored => {
+ train => $next_train,
+ dep => epoch_to_dt($next_departure)
+ ->strftime('%d.%m.%Y %H:%M'),
+ id => $next_id,
+ },
+ }
+ );
+ }
+ else {
+ $interchange_real
+ += ( $next_departure - $journey->{rt_arr_ts} ) / 60;
+ }
+ }
+ else {
+ $num_journeys++;
+ }
+ $next_departure = $journey->{rt_dep_ts};
+ $next_id = $journey->{id};
+ $next_train
+ = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),;
+ }
+ my $ret = {
+ km_route => $km_route,
+ km_beeline => $km_beeline,
+ num_trains => $num_trains,
+ num_journeys => $num_journeys,
+ min_travel_sched => $min_travel_sched,
+ min_travel_real => $min_travel_real,
+ min_interchange_real => $interchange_real,
+ delay_dep => $delay_dep,
+ delay_arr => $delay_arr,
+ inconsistencies => \@inconsistencies,
+ };
+ for my $key (
+ qw(min_travel_sched min_travel_real min_interchange_real delay_dep delay_arr)
+ )
+ {
+ my $strf_key = $key . '_strf';
+ my $value = $ret->{$key};
+ $ret->{$strf_key} = q{};
+ if ( $ret->{$key} < 0 ) {
+ $ret->{$strf_key} .= '-';
+ $value *= -1;
+ }
+ $ret->{$strf_key} .= sprintf( '%02d:%02d', $value / 60, $value % 60 );
+ }
+ return $ret;
+}
+
+sub get_stats {
+ my ( $self, %opt ) = @_;
+
+ if ( $opt{cancelled} ) {
+ $self->{log}
+ ->warn('get_journey_stats called with illegal option cancelled => 1');
+ return {};
+ }
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $year = $opt{year} // 0;
+ my $month = $opt{month} // 0;
+
+ # Assumption: If the stats cache contains an entry it is up-to-date.
+ # -> Cache entries must be explicitly invalidated whenever the user
+ # checks out of a train or manually edits/adds a journey.
+
+ if (
+ not $opt{write_only}
+ and not $opt{review}
+ and my $stats = $self->stats_cache->get(
+ uid => $uid,
+ db => $db,
+ year => $year,
+ month => $month
+ )
+ )
+ {
+ return $stats;
+ }
+
+ my $interval_start = DateTime->new(
+ time_zone => 'Europe/Berlin',
+ year => 2000,
+ month => 1,
+ day => 1,
+ hour => 0,
+ minute => 0,
+ second => 0,
+ );
+
+ # I wonder if people will still be traveling by train in the year 3000
+ my $interval_end = $interval_start->clone->add( years => 1000 );
+
+ if ( $opt{year} and $opt{month} ) {
+ $interval_start->set(
+ year => $opt{year},
+ month => $opt{month}
+ );
+ $interval_end = $interval_start->clone->add( months => 1 );
+ }
+ elsif ( $opt{year} ) {
+ $interval_start->set( year => $opt{year} );
+ $interval_end = $interval_start->clone->add( years => 1 );
+ }
+
+ my @journeys = $self->get(
+ uid => $uid,
+ cancelled => 0,
+ verbose => 1,
+ with_polyline => 1,
+ after => $interval_start,
+ before => $interval_end
+ );
+ my $stats = $self->compute_stats(@journeys);
+
+ $self->stats_cache->add(
+ uid => $uid,
+ db => $db,
+ year => $year,
+ month => $month,
+ stats => $stats
+ );
+
+ if ( $opt{review} ) {
+ my @cancelled_journeys = $self->get(
+ uid => $uid,
+ cancelled => 1,
+ verbose => 1,
+ after => $interval_start,
+ before => $interval_end
+ );
+ return ( $stats,
+ $self->compute_review( $stats, @journeys, @cancelled_journeys ) );
+ }
+
+ return $stats;
+}
+
+sub get_latest_dest_id {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ if (
+ my $id = $self->{in_transit}->get_checkout_station_id(
+ uid => $uid,
+ db => $db
+ )
+ )
+ {
+ return $id;
+ }
+
+ return $self->get_latest_checkout_station_id(
+ uid => $uid,
+ db => $db
+ );
+}
+
+sub get_connection_targets {
+ my ( $self, %opt ) = @_;
+
+ 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;
+
+ if ( $opt{destination_name} ) {
+ return (
+ [],
+ [ { eva => $opt{eva}, name => $opt{destination_name} } ]
+ );
+ }
+
+ my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt);
+
+ if ( not $dest_id ) {
+ return ( [], [] );
+ }
+
+ my $dest_ids = [ $dest_id, $self->{stations}->get_meta( eva => $dest_id ) ];
+
+ my $res = $db->select(
+ 'journeys',
+ 'count(checkout_station_id) as count, checkout_station_id as dest',
+ {
+ user_id => $uid,
+ checkin_station_id => $dest_ids,
+ real_departure => { '>', $threshold }
+ },
+ {
+ group_by => ['checkout_station_id'],
+ order_by => { -desc => 'count' }
+ }
+ );
+ my @destinations
+ = $res->hashes->grep( sub { shift->{count} >= $min_count } )
+ ->map( sub { shift->{dest} } )->each;
+ @destinations = $self->{stations}->get_by_evas(@destinations);
+ return ( $dest_ids, \@destinations );
+}
+
+sub update_visibility {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $visibility;
+
+ if ( $opt{visibility} and $visibility_atoi{ $opt{visibility} } ) {
+ $visibility = $visibility_atoi{ $opt{visibility} };
+ }
+
+ $db->update(
+ 'journeys',
+ { visibility => $visibility },
+ {
+ user_id => $uid,
+ id => $opt{id}
+ }
+ );
+}
+
+1;
diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm
new file mode 100644
index 0000000..ac4019c
--- /dev/null
+++ b/lib/Travelynx/Model/Stations.pm
@@ -0,0 +1,199 @@
+package Travelynx::Model::Stations;
+
+# Copyright (C) 2022 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub add_or_update {
+ my ( $self, %opt ) = @_;
+ my $stop = $opt{stop};
+ my $loc = $stop->loc;
+ my $source = 1;
+ my $db = $opt{db} // $self->{pg}->db;
+
+ if ( my $s = $self->get_by_eva( $loc->eva, db => $db ) ) {
+ if ( $source == 1 and $s->{source} == 0 and not $s->{archived} ) {
+ return;
+ }
+ $db->update(
+ 'stations',
+ {
+ name => $loc->name,
+ lat => $loc->lat,
+ lon => $loc->lon,
+ source => $source,
+ archived => 0
+ },
+ { eva => $loc->eva }
+ );
+ return;
+ }
+ $db->insert(
+ 'stations',
+ {
+ eva => $loc->eva,
+ name => $loc->name,
+ lat => $loc->lat,
+ lon => $loc->lon,
+ source => $source,
+ archived => 0
+ }
+ );
+}
+
+sub add_meta {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $eva = $opt{eva};
+ my @meta = @{ $opt{meta} };
+
+ for my $meta (@meta) {
+ if ( $meta != $eva ) {
+ $db->insert(
+ 'related_stations',
+ {
+ eva => $eva,
+ meta => $meta
+ },
+ { on_conflict => undef }
+ );
+ }
+ }
+}
+
+sub get_db_iterator {
+ my ($self) = @_;
+
+ return $self->{pg}->db->select( 'stations', '*' );
+}
+
+sub get_meta {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $eva = $opt{eva};
+
+ my $res = $db->select( 'related_stations', ['meta'], { eva => $eva } );
+ my @ret;
+
+ while ( my $row = $res->hash ) {
+ push( @ret, $row->{meta} );
+ }
+
+ return @ret;
+}
+
+sub get_for_autocomplete {
+ my ($self) = @_;
+
+ my $res = $self->{pg}->db->select( 'stations', ['name'] );
+ my %ret;
+
+ while ( my $row = $res->hash ) {
+ $ret{ $row->{name} } = undef;
+ }
+
+ return \%ret;
+}
+
+# Fast
+sub get_by_eva {
+ my ( $self, $eva, %opt ) = @_;
+
+ if ( not $eva ) {
+ return;
+ }
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { eva => $eva } )->hash;
+}
+
+# Fast
+sub get_by_evas {
+ my ( $self, @evas ) = @_;
+
+ my @ret
+ = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } )
+ ->hashes->each;
+ return @ret;
+}
+
+# Slow
+sub get_latlon_by_name {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my %location;
+ my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] );
+ while ( my $row = $res->hash ) {
+ $location{ $row->{name} } = [ $row->{lat}, $row->{lon} ];
+ }
+ return \%location;
+}
+
+# Slow
+sub get_by_name {
+ my ( $self, $name, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { name => $name }, { limit => 1 } )
+ ->hash;
+}
+
+# Slow
+sub get_by_names {
+ my ( $self, @names ) = @_;
+
+ my @ret
+ = $self->{pg}->db->select( 'stations', '*', { name => { '=', \@names } } )
+ ->hashes->each;
+ return @ret;
+}
+
+# Slow
+sub get_by_ds100 {
+ my ( $self, $ds100, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } )
+ ->hash;
+}
+
+# Can be slow
+sub search {
+ my ( $self, $identifier, %opt ) = @_;
+
+ if ( $identifier =~ m{ ^ \d+ $ }x ) {
+ return $self->get_by_eva( $identifier, %opt )
+ // $self->get_by_ds100( $identifier, %opt )
+ // $self->get_by_name( $identifier, %opt );
+ }
+
+ return $self->get_by_ds100( $identifier, %opt )
+ // $self->get_by_name( $identifier, %opt );
+}
+
+# Slow
+sub grep_unknown {
+ my ( $self, @stations ) = @_;
+
+ my %station = map { $_->{name} => 1 } $self->get_by_names(@stations);
+ my @unknown_stations = grep { not $station{$_} } @stations;
+
+ return @unknown_stations;
+}
+
+1;
diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm
new file mode 100644
index 0000000..25648cc
--- /dev/null
+++ b/lib/Travelynx/Model/Traewelling.pm
@@ -0,0 +1,243 @@
+package Travelynx::Model::Traewelling;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use DateTime;
+
+sub epoch_to_dt {
+ my ($epoch) = @_;
+
+ # Bugs (and user errors) may lead to undefined timestamps. Set them to
+ # 1970-01-01 to avoid crashing and show obviously wrong data instead.
+ $epoch //= 0;
+
+ return DateTime->from_epoch(
+ epoch => $epoch,
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
+ );
+
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub now {
+ return DateTime->now( time_zone => 'Europe/Berlin' );
+}
+
+sub link {
+ my ( $self, %opt ) = @_;
+
+ my $log = [ [ $self->now->epoch, "Erfolgreich mittels OAuth2 verbunden" ] ];
+
+ my $data = { log => $log };
+
+ my $user_entry = {
+ user_id => $opt{uid},
+ push_sync => 0,
+ pull_sync => 0,
+ token => $opt{token},
+ refresh_token => $opt{refresh_token},
+ expiry => epoch_to_dt( $self->now->epoch + $opt{expires_in} ),
+ data => JSON->new->encode($data),
+ };
+
+ $self->{pg}->db->insert(
+ 'traewelling',
+ $user_entry,
+ {
+ on_conflict => \
+'(user_id) do update set token = EXCLUDED.token, refresh_token = EXCLUDED.refresh_token, expiry = EXCLUDED.expiry, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null'
+ }
+ );
+
+ return $user_entry;
+}
+
+sub set_user {
+ my ( $self, %opt ) = @_;
+
+ my $res_h
+ = $self->{pg}
+ ->db->select( 'traewelling', 'data', { user_id => $opt{uid} } )
+ ->expand->hash;
+
+ $res_h->{data}{user_id} = $opt{trwl_id};
+ $res_h->{data}{screen_name} = $opt{screen_name};
+ $res_h->{data}{user_name} = $opt{user_name};
+
+ $self->{pg}->db->update(
+ 'traewelling',
+ { data => JSON->new->encode( $res_h->{data} ) },
+ { user_id => $opt{uid} }
+ );
+}
+
+sub unlink {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+
+ $self->{pg}->db->delete( 'traewelling', { user_id => $uid } );
+}
+
+sub get {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h
+ = $db->select( 'traewelling_str', '*', { user_id => $uid } )
+ ->expand->hash;
+
+ $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} );
+ for my $log_entry ( @{ $res_h->{data}{log} // [] } ) {
+ $log_entry->[0] = epoch_to_dt( $log_entry->[0] );
+ }
+ $res_h->{expires_on}
+ = epoch_to_dt( $res_h->{expiry_ts} // $res_h->{data}{expires} );
+
+ my $expires_in = ( $res_h->{expiry_ts} // $res_h->{data}{expires} // 0 )
+ - $self->now->epoch;
+
+ if ( $expires_in < 0 ) {
+ $res_h->{expired} = 1;
+ }
+ elsif ( $expires_in < 14 * 24 * 3600 ) {
+ $res_h->{expiring} = 1;
+ }
+
+ return $res_h;
+}
+
+sub log {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $message = $opt{message};
+ my $is_error = $opt{is_error};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $res_h
+ = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
+ splice( @{ $res_h->{data}{log} // [] }, 9 );
+ unshift(
+ @{ $res_h->{data}{log} },
+ [ $self->now->epoch, $message, $opt{status_id} ]
+ );
+
+ if ($is_error) {
+ $res_h->{data}{error} = $message;
+ }
+ $db->update(
+ 'traewelling',
+ {
+ errored => $is_error ? 1 : 0,
+ latest_run => $self->now,
+ data => JSON->new->encode( $res_h->{data} )
+ },
+ { user_id => $uid }
+ );
+}
+
+sub set_latest_pull_status_id {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $status_id = $opt{status_id};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h
+ = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
+
+ $res_h->{data}{latest_pull_status_id} = $status_id;
+
+ $db->update(
+ 'traewelling',
+ { data => JSON->new->encode( $res_h->{data} ) },
+ { user_id => $uid }
+ );
+}
+
+sub set_latest_push_ts {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $ts = $opt{ts};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h
+ = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
+
+ $res_h->{data}{latest_push_ts} = $ts;
+
+ $db->update(
+ 'traewelling',
+ { data => JSON->new->encode( $res_h->{data} ) },
+ { user_id => $uid }
+ );
+}
+
+sub set_sync {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h
+ = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash;
+
+ $res_h->{data}{toot} = $opt{toot};
+ $res_h->{data}{tweet} = $opt{tweet};
+
+ $db->update(
+ 'traewelling',
+ {
+ push_sync => $opt{push_sync},
+ pull_sync => $opt{pull_sync},
+ data => JSON->new->encode( $res_h->{data} ),
+ },
+ { user_id => $uid }
+ );
+}
+
+sub get_pushable_accounts {
+ my ($self) = @_;
+ my $res = $self->{pg}->db->query(
+ qq{select t.user_id as uid, t.token as token, t.data as data,
+ i.user_data as user_data,
+ i.dep_eva as dep_eva, i.arr_eva as arr_eva,
+ i.data as journey_data, i.train_type as train_type,
+ i.train_line as train_line, i.train_no as train_no,
+ i.checkin_ts as checkin_ts,
+ i.sched_dep_ts as dep_ts,
+ i.sched_arr_ts as arr_ts,
+ i.effective_visibility as visibility
+ from traewelling as t
+ join in_transit_str as i on t.user_id = i.user_id
+ where t.push_sync = True
+ and i.arr_eva is not null
+ and i.cancelled = False
+ }
+ );
+ return $res->expand->hashes->each;
+}
+
+sub get_pull_accounts {
+ my ($self) = @_;
+ my $res = $self->{pg}->db->select(
+ 'traewelling',
+ [ 'user_id', 'token', 'data' ],
+ { pull_sync => 1 }
+ );
+ return $res->expand->hashes->each;
+}
+
+1;
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
new file mode 100644
index 0000000..4602fa2
--- /dev/null
+++ b/lib/Travelynx/Model/Users.pm
@@ -0,0 +1,1160 @@
+package Travelynx::Model::Users;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
+use DateTime;
+use JSON;
+
+my %visibility_itoa = (
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+);
+
+my %visibility_atoi = (
+ public => 100,
+ travelynx => 80,
+ followers => 60,
+ unlisted => 30,
+ private => 10,
+);
+
+my %predicate_itoa = (
+ 1 => 'follows',
+ 2 => 'requests_follow',
+ 3 => 'is_blocked_by',
+);
+
+my %predicate_atoi = (
+ follows => 1,
+ requests_follow => 2,
+ is_blocked_by => 3,
+);
+
+my @sb_templates = (
+ undef,
+ [ 'DBF', 'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ],
+ [ 'bahn.expert', 'https://bahn.expert/{name}#{id}' ],
+ [ 'DBF HAFAS', 'https://dbf.finalrewind.org/{name}?rt=1&hafas=1#{tt}{tn}' ],
+ [ 'bahn.expert/regional', 'https://bahn.expert/regional/{name}#{id}' ],
+);
+
+my %token_id = (
+ status => 1,
+ history => 2,
+ travel => 3,
+ import => 4,
+);
+my @token_types = (qw(status history travel import));
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+sub hash_password {
+ my ( $self, $password ) = @_;
+ my @salt_bytes = map { int( rand(255) ) + 1 } ( 1 .. 16 );
+ my $salt = en_base64( pack( 'C[16]', @salt_bytes ) );
+
+ return bcrypt( substr( $password, 0, 10000 ), '$2a$12$' . $salt );
+}
+
+sub get_token_id {
+ my ( $self, $type ) = @_;
+
+ return $token_id{$type};
+}
+
+sub mark_seen {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ $db->update(
+ 'users',
+ {
+ last_seen => DateTime->now( time_zone => 'Europe/Berlin' ),
+ deletion_notified => undef
+ },
+ { id => $uid }
+ );
+}
+
+sub mark_deletion_notified {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ $db->update(
+ 'users',
+ {
+ deletion_notified => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
+ { id => $uid }
+ );
+}
+
+sub verify_registration_token {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ my $res = $db->select(
+ 'pending_registrations',
+ 'count(*) as count',
+ {
+ user_id => $uid,
+ token => $token
+ }
+ );
+
+ if ( $res->hash->{count} ) {
+ $db->update( 'users', { status => 1 }, { id => $uid } );
+ $db->delete( 'pending_registrations', { user_id => $uid } );
+ if ($tx) {
+ $tx->commit;
+ }
+ return 1;
+ }
+ return;
+}
+
+sub get_api_token {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $token = {};
+ my $res = $db->select( 'tokens', [ 'type', 'token' ], { user_id => $uid } );
+
+ for my $entry ( $res->hashes->each ) {
+ $token->{ $token_types[ $entry->{type} - 1 ] }
+ = $entry->{token};
+ }
+
+ return $token;
+}
+
+sub get_uid_by_name_and_mail {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $name = $opt{name};
+ my $email = $opt{email};
+
+ my $res = $db->select(
+ 'users',
+ ['id'],
+ {
+ name => $name,
+ email => $email,
+ status => 1
+ }
+ );
+
+ if ( my $user = $res->hash ) {
+ return $user->{id};
+ }
+ return;
+}
+
+sub get_privacy_by {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my %where;
+
+ if ( $opt{name} ) {
+ $where{name} = $opt{name};
+ }
+ else {
+ $where{id} = $opt{uid};
+ }
+
+ my $res = $db->select(
+ 'users',
+ [ 'id', 'name', 'public_level', 'accept_follows' ],
+ { %where, status => 1 }
+ );
+
+ if ( my $user = $res->hash ) {
+ return {
+ id => $user->{id},
+ name => $user->{name},
+ default_visibility => $user->{public_level} & 0x7f,
+ default_visibility_str =>
+ $visibility_itoa{ $user->{public_level} & 0x7f },
+ comments_visible => $user->{public_level} & 0x80 ? 1 : 0,
+ past_visibility => ( $user->{public_level} & 0x7f00 ) >> 8,
+ past_visibility_str =>
+ $visibility_itoa{ ( $user->{public_level} & 0x7f00 ) >> 8 },
+ past_status => $user->{public_level} & 0x08000 ? 1 : 0,
+ past_all => $user->{public_level} & 0x10000 ? 1 : 0,
+ accept_follows => $user->{accept_follows} == 2 ? 1 : 0,
+ accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0,
+ };
+ }
+ return;
+}
+
+sub set_privacy {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $public_level = $opt{level};
+
+ if ( not defined $public_level and defined $opt{default_visibility} ) {
+ $public_level
+ = ( $opt{default_visibility} & 0x7f )
+ | ( $opt{comments_visible} ? 0x80 : 0 )
+ | ( ( $opt{past_visibility} & 0x7f ) << 8 )
+ | ( $opt{past_status} ? 0x08000 : 0 )
+ | ( $opt{past_all} ? 0x10000 : 0 );
+ }
+
+ $db->update( 'users', { public_level => $public_level }, { id => $uid } );
+}
+
+sub set_social {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $accept_follows = 0;
+
+ if ( $opt{accept_follows} ) {
+ $accept_follows = 2;
+ }
+ elsif ( $opt{accept_follow_requests} ) {
+ $accept_follows = 1;
+ }
+
+ $db->update(
+ 'users',
+ { accept_follows => $accept_follows },
+ { id => $uid }
+ );
+}
+
+sub mark_for_password_reset {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ my $res = $db->select(
+ 'pending_passwords',
+ 'count(*) as count',
+ { user_id => $uid }
+ );
+ if ( $res->hash->{count} ) {
+ return 'in progress';
+ }
+
+ $db->insert(
+ 'pending_passwords',
+ {
+ user_id => $uid,
+ token => $token,
+ requested_at => DateTime->now( time_zone => 'Europe/Berlin' )
+ }
+ );
+
+ return undef;
+}
+
+sub verify_password_token {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ my $res = $db->select(
+ 'pending_passwords',
+ 'count(*) as count',
+ {
+ user_id => $uid,
+ token => $token
+ }
+ );
+
+ if ( $res->hash->{count} ) {
+ return 1;
+ }
+ return;
+}
+
+sub mark_for_mail_change {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $email = $opt{email};
+ my $token = $opt{token};
+
+ $db->insert(
+ 'pending_mails',
+ {
+ user_id => $uid,
+ email => $email,
+ token => $token,
+ requested_at => DateTime->now( time_zone => 'Europe/Berlin' )
+ },
+ {
+ on_conflict => \
+'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, requested_at = EXCLUDED.requested_at'
+ },
+ );
+}
+
+sub change_mail_with_token {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ my $tx = $db->begin;
+
+ my $res_h = $db->select(
+ 'pending_mails',
+ ['email'],
+ {
+ user_id => $uid,
+ token => $token
+ }
+ )->hash;
+
+ if ($res_h) {
+ $db->update( 'users', { email => $res_h->{email} }, { id => $uid } );
+ $db->delete( 'pending_mails', { user_id => $uid } );
+ $tx->commit;
+ return 1;
+ }
+ return;
+}
+
+sub is_name_invalid {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $name = $opt{name};
+
+ if ( not length($name) ) {
+ return 'user_empty';
+ }
+
+ if ( $name !~ m{ ^ [0-9a-zA-Z_-]+ $ }x ) {
+ return 'user_format';
+ }
+
+ if (
+ $self->user_name_exists(
+ db => $db,
+ name => $name
+ )
+ )
+ {
+ return 'user_collision';
+ }
+
+ return;
+}
+
+sub change_name {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ eval { $db->update( 'users', { name => $opt{name} }, { id => $uid } ); };
+
+ if ($@) {
+ return 0;
+ }
+
+ return 1;
+}
+
+sub remove_password_token {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $token = $opt{token};
+
+ $db->delete(
+ 'pending_passwords',
+ {
+ user_id => $uid,
+ token => $token
+ }
+ );
+}
+
+sub get {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $user = $db->select(
+ 'users',
+ 'id, name, status, public_level, email, '
+ . 'external_services, accept_follows, notifications, '
+ . 'extract(epoch from registered_at) as registered_at_ts, '
+ . 'extract(epoch from last_seen) as last_seen_ts, '
+ . 'extract(epoch from deletion_requested) as deletion_requested_ts',
+ { id => $uid }
+ )->hash;
+ if ($user) {
+ return {
+ id => $user->{id},
+ name => $user->{name},
+ 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,
+ default_visibility_str =>
+ $visibility_itoa{ $user->{public_level} & 0x7f },
+ comments_visible => $user->{public_level} & 0x80 ? 1 : 0,
+ past_visibility => ( $user->{public_level} & 0x7f00 ) >> 8,
+ past_visibility_str =>
+ $visibility_itoa{ ( $user->{public_level} & 0x7f00 ) >> 8 },
+ past_status => $user->{public_level} & 0x08000 ? 1 : 0,
+ past_all => $user->{public_level} & 0x10000 ? 1 : 0,
+ email => $user->{email},
+ sb_name => $user->{external_services}
+ ? $sb_templates[ $user->{external_services} & 0x07 ][0]
+ : undef,
+ sb_template => $user->{external_services}
+ ? $sb_templates[ $user->{external_services} & 0x07 ][1]
+ : undef,
+ registered_at => DateTime->from_epoch(
+ epoch => $user->{registered_at_ts},
+ time_zone => 'Europe/Berlin'
+ ),
+ last_seen => DateTime->from_epoch(
+ epoch => $user->{last_seen_ts},
+ time_zone => 'Europe/Berlin'
+ ),
+ deletion_requested => $user->{deletion_requested_ts}
+ ? DateTime->from_epoch(
+ epoch => $user->{deletion_requested_ts},
+ time_zone => 'Europe/Berlin'
+ )
+ : undef,
+ };
+ }
+ return undef;
+}
+
+sub get_login_data {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $name = $opt{name};
+
+ my $res_h = $db->select(
+ 'users',
+ 'id, name, status, password as password_hash',
+ { name => $name }
+ )->hash;
+
+ return $res_h;
+}
+
+sub add {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $user_name = $opt{name};
+ my $email = $opt{email};
+ my $token = $opt{token};
+ my $password = $self->hash_password( $opt{password} );
+
+ # This helper must be called during a transaction, as user creation
+ # may fail even after the database entry has been generated, e.g. if
+ # the registration mail cannot be sent. We therefore use $db (the
+ # database handle performing the transaction) instead of $self->pg->db
+ # (which may be a new handle not belonging to the transaction).
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my $res = $db->insert(
+ 'users',
+ {
+ name => $user_name,
+ status => 0,
+ public_level => $visibility_atoi{unlisted}
+ | ( $visibility_atoi{unlisted} << 8 ),
+ email => $email,
+ password => $password,
+ registered_at => $now,
+ last_seen => $now,
+ },
+ { returning => 'id' }
+ );
+ my $uid = $res->hash->{id};
+
+ $db->insert(
+ 'pending_registrations',
+ {
+ user_id => $uid,
+ token => $token
+ }
+ );
+
+ return $uid;
+}
+
+sub flag_deletion {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ $db->update(
+ 'users',
+ { deletion_requested => $now },
+ {
+ id => $uid,
+ }
+ );
+}
+
+sub unflag_deletion {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ $db->update(
+ 'users',
+ {
+ deletion_requested => undef,
+ },
+ {
+ id => $uid,
+ }
+ );
+}
+
+sub delete {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ my %res;
+
+ $res{tokens} = $db->delete( 'tokens', { user_id => $uid } );
+ $res{stats} = $db->delete( 'journey_stats', { user_id => $uid } );
+ $res{journeys} = $db->delete( 'journeys', { user_id => $uid } );
+ $res{transit} = $db->delete( 'in_transit', { user_id => $uid } );
+ $res{hooks} = $db->delete( 'webhooks', { user_id => $uid } );
+ $res{trwl} = $db->delete( 'traewelling', { user_id => $uid } );
+ $res{password} = $db->delete( 'pending_passwords', { user_id => $uid } );
+ $res{relations} = $db->delete( 'relations',
+ [ { subject_id => $uid }, { object_id => $uid } ] );
+ $res{users} = $db->delete( 'users', { id => $uid } );
+
+ for my $key ( keys %res ) {
+ $res{$key} = $res{$key}->rows;
+ }
+
+ if ( $res{users} != 1 ) {
+ die("Deleted $res{users} rows from users, expected 1. Rolling back.\n");
+ }
+
+ if ($tx) {
+ $tx->commit;
+ }
+
+ return \%res;
+}
+
+sub set_password {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $password = $self->hash_password( $opt{password} );
+
+ $db->update( 'users', { password => $password }, { id => $uid } );
+}
+
+sub user_name_exists {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $user_name = $opt{name};
+
+ my $count
+ = $db->select( 'users', 'count(*) as count', { name => $user_name } )
+ ->hash->{count};
+
+ if ($count) {
+ return 1;
+ }
+ return 0;
+}
+
+sub mail_is_blacklisted {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $mail = $opt{email};
+
+ my $count = $db->select(
+ 'users',
+ 'count(*) as count',
+ {
+ email => $mail,
+ status => 0,
+ }
+ )->hash->{count};
+
+ if ($count) {
+ return 1;
+ }
+
+ $count = $db->select(
+ 'mail_blacklist',
+ 'count(*) as count',
+ {
+ email => $mail,
+ num_tries => { '>', 1 },
+ }
+ )->hash->{count};
+
+ if ($count) {
+ return 1;
+ }
+ return 0;
+}
+
+sub use_history {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $value = $opt{set};
+
+ if ($value) {
+ $db->update( 'users', { use_history => $value }, { id => $uid } );
+ }
+ else {
+ return $db->select( 'users', ['use_history'], { id => $uid } )
+ ->hash->{use_history};
+ }
+}
+
+sub use_external_services {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $value = $opt{set};
+
+ if ( defined $value ) {
+ if ( $value < 0 or $value > 4 ) {
+ $value = 0;
+ }
+ $db->update( 'users', { external_services => $value }, { id => $uid } );
+ }
+ else {
+ return $db->select( 'users', ['external_services'], { id => $uid } )
+ ->hash->{external_services};
+ }
+}
+
+sub get_webhook {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $res_h = $db->select( 'webhooks_str', '*', { user_id => $uid } )->hash;
+
+ $res_h->{latest_run} = DateTime->from_epoch(
+ epoch => $res_h->{latest_run_ts} // 0,
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
+ );
+
+ return $res_h;
+}
+
+sub set_webhook {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+
+ if ( $opt{token} ) {
+ $opt{token} =~ tr{\r\n}{}d;
+ }
+
+ my $res = $db->insert(
+ 'webhooks',
+ {
+ user_id => $opt{uid},
+ enabled => $opt{enabled},
+ url => $opt{url},
+ token => $opt{token}
+ },
+ {
+ on_conflict => \
+'(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null'
+ }
+ );
+}
+
+sub update_webhook_status {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $url = $opt{url};
+ my $success = $opt{success};
+ my $text = $opt{text};
+
+ if ( length($text) > 1000 ) {
+ $text = substr( $text, 0, 1000 ) . '…';
+ }
+
+ $db->update(
+ 'webhooks',
+ {
+ errored => $success ? 0 : 1,
+ latest_run => DateTime->now( time_zone => 'Europe/Berlin' ),
+ output => $text,
+ },
+ {
+ user_id => $uid,
+ url => $url
+ }
+ );
+}
+
+sub set_profile {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $profile = $opt{profile};
+
+ $db->update(
+ 'users',
+ { profile => JSON->new->encode($profile) },
+ { id => $uid }
+ );
+}
+
+sub get_profile {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ return $db->select( 'users', ['profile'], { id => $uid } )
+ ->expand->hash->{profile};
+}
+
+sub get_relation {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $subject = $opt{subject};
+ my $object = $opt{object};
+
+ my $res_h = $db->select(
+ 'relations',
+ ['predicate'],
+ {
+ subject_id => $subject,
+ object_id => $object,
+ }
+ )->hash;
+
+ if ($res_h) {
+ return $predicate_itoa{ $res_h->{predicate} };
+ }
+ return;
+
+ #my $res_h = $db->select( 'relations', ['subject_id', 'predicate'],
+ # { subject_id => [$uid, $target], object_id => [$target, $target] } )->hash;
+}
+
+sub update_notifications {
+ my ( $self, %opt ) = @_;
+
+ # must be called inside a transaction, so $opt{db} is mandatory.
+ my $db = $opt{db};
+ my $uid = $opt{uid};
+
+ my $has_follow_requests = $opt{has_follow_requests}
+ // $self->has_follow_requests(
+ db => $db,
+ uid => $uid
+ );
+
+ my $notifications
+ = $db->select( 'users', ['notifications'], { id => $uid } )
+ ->hash->{notifications};
+ if ($has_follow_requests) {
+ $notifications |= 0x01;
+ }
+ else {
+ $notifications &= ~0x01;
+ }
+ $db->update( 'users', { notifications => $notifications }, { id => $uid } );
+}
+
+sub follow {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $target = $opt{target};
+
+ $db->insert(
+ 'relations',
+ {
+ subject_id => $uid,
+ predicate => $predicate_atoi{follows},
+ object_id => $target,
+ ts => DateTime->now( time_zone => 'Europe/Berlin' ),
+ }
+ );
+}
+
+sub request_follow {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $target = $opt{target};
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ $db->insert(
+ 'relations',
+ {
+ subject_id => $uid,
+ predicate => $predicate_atoi{requests_follow},
+ object_id => $target,
+ ts => DateTime->now( time_zone => 'Europe/Berlin' ),
+ }
+ );
+ $self->update_notifications(
+ db => $db,
+ uid => $target,
+ has_follow_requests => 1,
+ );
+
+ if ($tx) {
+ $tx->commit;
+ }
+}
+
+sub accept_follow_request {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $applicant = $opt{applicant};
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ $db->update(
+ 'relations',
+ {
+ predicate => $predicate_atoi{follows},
+ ts => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
+ {
+ subject_id => $applicant,
+ predicate => $predicate_atoi{requests_follow},
+ object_id => $uid
+ }
+ );
+ $self->update_notifications(
+ db => $db,
+ uid => $uid
+ );
+
+ if ($tx) {
+ $tx->commit;
+ }
+}
+
+sub reject_follow_request {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $applicant = $opt{applicant};
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ $db->delete(
+ 'relations',
+ {
+ subject_id => $applicant,
+ predicate => $predicate_atoi{requests_follow},
+ object_id => $uid
+ }
+ );
+ $self->update_notifications(
+ db => $db,
+ uid => $uid
+ );
+
+ if ($tx) {
+ $tx->commit;
+ }
+}
+
+sub cancel_follow_request {
+ my ( $self, %opt ) = @_;
+
+ $self->reject_follow_request(
+ db => $opt{db},
+ uid => $opt{target},
+ applicant => $opt{uid},
+ );
+}
+
+sub unfollow {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $target = $opt{target};
+
+ $db->delete(
+ 'relations',
+ {
+ subject_id => $uid,
+ predicate => $predicate_atoi{follows},
+ object_id => $target
+ }
+ );
+}
+
+sub remove_follower {
+ my ( $self, %opt ) = @_;
+
+ $self->unfollow(
+ db => $opt{db},
+ uid => $opt{follower},
+ target => $opt{uid},
+ );
+}
+
+sub block {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $target = $opt{target};
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ $db->insert(
+ 'relations',
+ {
+ subject_id => $target,
+ predicate => $predicate_atoi{is_blocked_by},
+ object_id => $uid,
+ ts => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
+ {
+ on_conflict => \
+'(subject_id, object_id) do update set predicate = EXCLUDED.predicate'
+ },
+ );
+ $self->update_notifications(
+ db => $db,
+ uid => $uid
+ );
+
+ if ($tx) {
+ $tx->commit;
+ }
+}
+
+sub unblock {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $target = $opt{target};
+
+ $db->delete(
+ 'relations',
+ {
+ subject_id => $target,
+ predicate => $predicate_atoi{is_blocked_by},
+ object_id => $uid
+ },
+ );
+}
+
+sub get_followers {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $res = $db->select(
+ 'followers',
+ [ 'id', 'name', 'accept_follows', 'inverse_predicate' ],
+ { self_id => $uid }
+ );
+
+ my @ret;
+ while ( my $row = $res->hash ) {
+ push(
+ @ret,
+ {
+ id => $row->{id},
+ name => $row->{name},
+ following_back => (
+ $row->{inverse_predicate}
+ and $row->{inverse_predicate} == $predicate_atoi{follows}
+ ) ? 1 : 0,
+ followback_requested => (
+ $row->{inverse_predicate}
+ and $row->{inverse_predicate}
+ == $predicate_atoi{requests_follow}
+ ) ? 1 : 0,
+ can_follow_back => (
+ not $row->{inverse_predicate}
+ and $row->{accept_follows} == 2
+ ) ? 1 : 0,
+ can_request_follow_back => (
+ not $row->{inverse_predicate}
+ and $row->{accept_follows} == 1
+ ) ? 1 : 0,
+ }
+ );
+ }
+ return @ret;
+}
+
+sub has_followers {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ return $db->select( 'followers', 'count(*) as count', { self_id => $uid } )
+ ->hash->{count};
+}
+
+sub get_follow_requests {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $table = $opt{sent} ? 'tx_follow_requests' : 'rx_follow_requests';
+
+ my $res
+ = $db->select( $table, [ 'id', 'name' ], { self_id => $uid } );
+
+ return $res->hashes->each;
+}
+
+sub has_follow_requests {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $table = $opt{sent} ? 'tx_follow_requests' : 'rx_follow_requests';
+
+ return $db->select( $table, 'count(*) as count', { self_id => $uid } )
+ ->hash->{count};
+}
+
+sub get_followees {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $res = $db->select(
+ 'followees',
+ [ 'id', 'name', 'inverse_predicate' ],
+ { self_id => $uid }
+ );
+
+ my @ret;
+ while ( my $row = $res->hash ) {
+ push(
+ @ret,
+ {
+ id => $row->{id},
+ name => $row->{name},
+ following_back => (
+ $row->{inverse_predicate}
+ and $row->{inverse_predicate} == $predicate_atoi{follows}
+ ) ? 1 : 0,
+ }
+ );
+ }
+ return @ret;
+}
+
+sub has_followees {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ return $db->select( 'followees', 'count(*) as count', { self_id => $uid } )
+ ->hash->{count};
+}
+
+sub get_blocked_users {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ my $res
+ = $db->select( 'blocked_users', [ 'id', 'name' ], { self_id => $uid } );
+
+ return $res->hashes->each;
+}
+
+sub has_blocked_users {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+
+ return $db->select( 'blocked_users', 'count(*) as count',
+ { self_id => $uid } )->hash->{count};
+}
+
+1;
diff --git a/public/service-worker.js b/public/service-worker.js
index 1302609..b564d65 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,19 +1,18 @@
-const CACHE_NAME = 'static-cache-v32';
+const CACHE_NAME = 'static-cache-v72';
const FILES_TO_CACHE = [
'/favicon.ico',
'/offline.html',
- '/static/v32/css/light.min.css',
- '/static/v32/css/dark.min.css',
- '/static/v32/css/material-icons.css',
- '/static/v32/css/local.css',
- '/static/v32/fonts/MaterialIcons-Regular.woff2',
- '/static/v32/fonts/MaterialIcons-Regular.woff',
- '/static/v32/fonts/MaterialIcons-Regular.ttf',
- '/static/v32/js/jquery-3.4.1.min.js',
- '/static/v32/js/materialize.min.js',
- '/static/v32/js/travelynx-actions.min.js',
- '/static/v32/js/autocomplete.min.js',
- '/static/v32/js/geolocation.min.js',
+ '/static/v72/css/light.min.css',
+ '/static/v72/css/dark.min.css',
+ '/static/v72/css/material-icons.css',
+ '/static/v72/fonts/MaterialIcons-Regular.woff2',
+ '/static/v72/fonts/MaterialIcons-Regular.woff',
+ '/static/v72/fonts/MaterialIcons-Regular.ttf',
+ '/static/v72/js/jquery-3.4.1.min.js',
+ '/static/v72/js/materialize.min.js',
+ '/static/v72/js/travelynx-actions.min.js',
+ '/static/v72/js/autocomplete.min.js',
+ '/static/v72/js/geolocation.min.js',
];
self.addEventListener('install', (evt) => {
diff --git a/public/static/api.yml b/public/static/api.yml
new file mode 100644
index 0000000..e55c5f7
--- /dev/null
+++ b/public/static/api.yml
@@ -0,0 +1,267 @@
+# Copyright (C) 2020 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: CC0-1.0
+openapi: 3.0.3
+info:
+ title: travelynx
+ version: 0.1.0
+ description: Travelynx API
+servers:
+ - url: 'https://travelynx.de/api/v1'
+ - url: 'https://travellynx.de/api/v1'
+ - url: 'https://travelynx.finalrewind.org/api/v1'
+tags:
+ - name: Status
+ description: 'Query check-in and journey status'
+ - name: Travel
+ description: 'Check into and out of trains'
+ - name: Import
+ description: 'Import a journey'
+paths:
+ '/status/{token}':
+ get:
+ tags:
+ - Status
+ summary: Retrieves a user's status
+ parameters:
+ - in: path
+ name: token
+ schema:
+ $ref: '#/components/schemas/token'
+ required: true
+ description: Status Token
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/status'
+ '/travel':
+ post:
+ tags:
+ - Travel
+ summary: Check into or out of a train
+ requestBody:
+ content:
+ application/json:
+ schema:
+ oneOf:
+ - $ref: '#/components/schemas/checkinRequest'
+ - $ref: '#/components/schemas/checkoutRequest'
+ - $ref: '#/components/schemas/undoRequest'
+ examples:
+ checkin:
+ value: {"action": "checkin", "token": "FIXME"}
+ checkout:
+ value: {"action": "checkout", "token": "FIXME"}
+ undo:
+ value: {"action": "undo", "token": "FIXME"}
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/travelResponse'
+components:
+ schemas:
+ token:
+ type: string
+ description: Status Token as configured on Travelynx User Panel
+ status:
+ type: object
+ properties:
+ deprecated:
+ type: boolean
+ example: false
+ description: if true, this API version is no longer supported and will be removed in the future
+ checkedIn:
+ type: boolean
+ example: true
+ description: Is the user currently checked into a train?
+ fromStation:
+ $ref: '#/components/schemas/departureStatus'
+ toStation:
+ $ref: '#/components/schemas/arrivalStatus'
+ intermediateStops:
+ type: array
+ items:
+ $ref: '#/components/schemas/intermediateStop'
+ train:
+ $ref: '#/components/schemas/train'
+ actionTime:
+ type: number
+ example: 1556083434
+ description: checkin/checkout epoch
+ travelRequest:
+ type: object
+ discriminator:
+ propertyName: action
+ mapping:
+ checkin: checkinRequest
+ checkout: checkoutRequest
+ undo: undoRequest
+ properties:
+ token:
+ type: string
+ action:
+ type: string
+ enum: ['checkin', 'checkout', 'undo']
+ required:
+ - token
+ - action
+ checkinRequest:
+ allOf:
+ - $ref: '#/components/schemas/travelRequest'
+ - type: object
+ properties:
+ train:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "ICE"
+ no:
+ type: string
+ example: "209"
+ fromStation:
+ $ref: '#/components/schemas/station'
+ toStation:
+ $ref: '#/components/schemas/station'
+ comment:
+ type: string
+ required: ['train', 'fromStation']
+ checkoutRequest:
+ allOf:
+ - $ref: '#/components/schemas/travelRequest'
+ - type: object
+ properties:
+ force:
+ type: boolean
+ description: "If true: perform checkout now. May lead to log entries without arrival time"
+ default: false
+ toStation:
+ $ref: '#/components/schemas/station'
+ comment:
+ type: string
+ required: ['toStation']
+ undoRequest:
+ allOf:
+ - $ref: '#/components/schemas/travelRequest'
+ - type: object
+ travelResponse:
+ type: object
+ properties:
+ deprecated:
+ type: boolean
+ example: false
+ description: if true, this API version is no longer supported and will be removed in the future
+ success:
+ type: boolean
+ example: true
+ status:
+ $ref: '#/components/schemas/status'
+ departureStatus:
+ type: object
+ properties:
+ name:
+ type: string
+ example: "Essen Hbf"
+ ds100:
+ type: string
+ example: "EE"
+ uic:
+ type: number
+ example: 8000098
+ latitude:
+ type: number
+ example: 51.451355
+ longitude:
+ type: number
+ example: 7.014793
+ scheduledTime:
+ type: number
+ example: 1556083680
+ realtime:
+ type: number
+ example: 1556083680
+ arrivalStatus:
+ type: object
+ description: If journey destination is not yet known, all fields are null
+ nullable: true
+ properties:
+ name:
+ type: string
+ example: "Essen Stadtwald"
+ ds100:
+ type: string
+ example: "EESA"
+ uic:
+ type: number
+ example: 8001896
+ latitude:
+ type: number
+ example: 51.422853
+ longitude:
+ type: number
+ example: 7.023296
+ scheduledTime:
+ type: number
+ example: 1556083980
+ nullable: true
+ description: If arrival time is not yet known, this field is null
+ realtime:
+ type: number
+ example: 1556083980
+ nullable: true
+ description: If arrival time is not yet known, this field is null
+ intermediateStop:
+ type: object
+ properties:
+ name:
+ type: string
+ example: "Essen Süd"
+ scheduledArrival:
+ type: number
+ example: 1556083800
+ nullable: true
+ realArrival:
+ type: number
+ example: 1556083800
+ nullable: true
+ scheduledDeparture:
+ type: number
+ example: 1556083860
+ nullable: true
+ realDeparture:
+ type: number
+ example: 1556083860
+ nullable: true
+ train:
+ type: object
+ properties:
+ type:
+ type: string
+ example: "S"
+ line:
+ type: string
+ example: "6"
+ no:
+ type: string
+ example: "30634"
+ id:
+ type: string
+ example: "7512500863736016593"
+ description: IRIS-specific train ID
+ station:
+ oneOf:
+ - type: string
+ description: Station Name
+ example: "Essen Hbf"
+ - type: string
+ description: DS100 Code
+ example: "EE"
+ - type: number
+ description: EVA number
+ example: 8000098
diff --git a/public/static/css/dark.min.css b/public/static/css/dark.min.css
index 43a4b31..3594ca3 100644
--- a/public/static/css/dark.min.css
+++ b/public/static/css/dark.min.css
@@ -1,12 +1,8 @@
-.materialize-red{background-color:#e51c23!important}.materialize-red-text{color:#e51c23!important}.materialize-red.lighten-5{background-color:#fdeaeb!important}.materialize-red-text.text-lighten-5{color:#fdeaeb!important}.materialize-red.lighten-4{background-color:#f8c1c3!important}.materialize-red-text.text-lighten-4{color:#f8c1c3!important}.materialize-red.lighten-3{background-color:#f3989b!important}.materialize-red-text.text-lighten-3{color:#f3989b!important}.materialize-red.lighten-2{background-color:#ee6e73!important}.materialize-red-text.text-lighten-2{color:#ee6e73!important}.materialize-red.lighten-1{background-color:#ea454b!important}.materialize-red-text.text-lighten-1{color:#ea454b!important}.materialize-red.darken-1{background-color:#d0181e!important}.materialize-red-text.text-darken-1{color:#d0181e!important}.materialize-red.darken-2{background-color:#b9151b!important}.materialize-red-text.text-darken-2{color:#b9151b!important}.materialize-red.darken-3{background-color:#a21318!important}.materialize-red-text.text-darken-3{color:#a21318!important}.materialize-red.darken-4{background-color:#8b1014!important}.materialize-red-text.text-darken-4{color:#8b1014!important}.red{background-color:#f44336!important}.red-text{color:#f44336!important}.red.lighten-5{background-color:#ffebee!important}.red-text.text-lighten-5{color:#ffebee!important}.red.lighten-4{background-color:#ffcdd2!important}.red-text.text-lighten-4{color:#ffcdd2!important}.red.lighten-3{background-color:#ef9a9a!important}.red-text.text-lighten-3{color:#ef9a9a!important}.red.lighten-2{background-color:#e57373!important}.red-text.text-lighten-2{color:#e57373!important}.red.lighten-1{background-color:#ef5350!important}.red-text.text-lighten-1{color:#ef5350!important}.red.darken-1{background-color:#e53935!important}.red-text.text-darken-1{color:#e53935!important}.red.darken-2{background-color:#d32f2f!important}.red-text.text-darken-2{color:#d32f2f!important}.red.darken-3{background-color:#c62828!important}.red-text.text-darken-3{color:#c62828!important}.red.darken-4{background-color:#b71c1c!important}.red-text.text-darken-4{color:#b71c1c!important}.red.accent-1{background-color:#ff8a80!important}.red-text.text-accent-1{color:#ff8a80!important}.red.accent-2{background-color:#ff5252!important}.red-text.text-accent-2{color:#ff5252!important}.red.accent-3{background-color:#ff1744!important}.red-text.text-accent-3{color:#ff1744!important}.red.accent-4{background-color:#d50000!important}.red-text.text-accent-4{color:#d50000!important}.pink{background-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink.lighten-5{background-color:#fce4ec!important}.pink-text.text-lighten-5{color:#fce4ec!important}.pink.lighten-4{background-color:#f8bbd0!important}.pink-text.text-lighten-4{color:#f8bbd0!important}.pink.lighten-3{background-color:#f48fb1!important}.pink-text.text-lighten-3{color:#f48fb1!important}.pink.lighten-2{background-color:#f06292!important}.pink-text.text-lighten-2{color:#f06292!important}.pink.lighten-1{background-color:#ec407a!important}.pink-text.text-lighten-1{color:#ec407a!important}.pink.darken-1{background-color:#d81b60!important}.pink-text.text-darken-1{color:#d81b60!important}.pink.darken-2{background-color:#c2185b!important}.pink-text.text-darken-2{color:#c2185b!important}.pink.darken-3{background-color:#ad1457!important}.pink-text.text-darken-3{color:#ad1457!important}.pink.darken-4{background-color:#880e4f!important}.pink-text.text-darken-4{color:#880e4f!important}.pink.accent-1{background-color:#ff80ab!important}.pink-text.text-accent-1{color:#ff80ab!important}.pink.accent-2{background-color:#ff4081!important}.pink-text.text-accent-2{color:#ff4081!important}.pink.accent-3{background-color:#f50057!important}.pink-text.text-accent-3{color:#f50057!important}.pink.accent-4{background-color:#c51162!important}.pink-text.text-accent-4{color:#c51162!important}.purple{background-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple.lighten-5{background-color:#f3e5f5!important}.purple-text.text-lighten-5{color:#f3e5f5!important}.purple.lighten-4{background-color:#e1bee7!important}.purple-text.text-lighten-4{color:#e1bee7!important}.purple.lighten-3{background-color:#ce93d8!important}.purple-text.text-lighten-3{color:#ce93d8!important}.purple.lighten-2{background-color:#ba68c8!important}.purple-text.text-lighten-2{color:#ba68c8!important}.purple.lighten-1{background-color:#ab47bc!important}.purple-text.text-lighten-1{color:#ab47bc!important}.purple.darken-1{background-color:#8e24aa!important}.purple-text.text-darken-1{color:#8e24aa!important}.purple.darken-2{background-color:#7b1fa2!important}.purple-text.text-darken-2{color:#7b1fa2!important}.purple.darken-3{background-color:#6a1b9a!important}.purple-text.text-darken-3{color:#6a1b9a!important}.purple.darken-4{background-color:#4a148c!important}.purple-text.text-darken-4{color:#4a148c!important}.purple.accent-1{background-color:#ea80fc!important}.purple-text.text-accent-1{color:#ea80fc!important}.purple.accent-2{background-color:#e040fb!important}.purple-text.text-accent-2{color:#e040fb!important}.purple.accent-3{background-color:#d500f9!important}.purple-text.text-accent-3{color:#d500f9!important}.purple.accent-4{background-color:#a0f!important}.purple-text.text-accent-4{color:#a0f!important}.deep-purple{background-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple.lighten-5{background-color:#ede7f6!important}.deep-purple-text.text-lighten-5{color:#ede7f6!important}.deep-purple.lighten-4{background-color:#d1c4e9!important}.deep-purple-text.text-lighten-4{color:#d1c4e9!important}.deep-purple.lighten-3{background-color:#b39ddb!important}.deep-purple-text.text-lighten-3{color:#b39ddb!important}.deep-purple.lighten-2{background-color:#9575cd!important}.deep-purple-text.text-lighten-2{color:#9575cd!important}.deep-purple.lighten-1{background-color:#7e57c2!important}.deep-purple-text.text-lighten-1{color:#7e57c2!important}.deep-purple.darken-1{background-color:#5e35b1!important}.deep-purple-text.text-darken-1{color:#5e35b1!important}.deep-purple.darken-2{background-color:#512da8!important}.deep-purple-text.text-darken-2{color:#512da8!important}.deep-purple.darken-3{background-color:#4527a0!important}.deep-purple-text.text-darken-3{color:#4527a0!important}.deep-purple.darken-4{background-color:#311b92!important}.deep-purple-text.text-darken-4{color:#311b92!important}.deep-purple.accent-1{background-color:#b388ff!important}.deep-purple-text.text-accent-1{color:#b388ff!important}.deep-purple.accent-2{background-color:#7c4dff!important}.deep-purple-text.text-accent-2{color:#7c4dff!important}.deep-purple.accent-3{background-color:#651fff!important}.deep-purple-text.text-accent-3{color:#651fff!important}.deep-purple.accent-4{background-color:#6200ea!important}.deep-purple-text.text-accent-4{color:#6200ea!important}.indigo{background-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo.lighten-5{background-color:#e8eaf6!important}.indigo-text.text-lighten-5{color:#e8eaf6!important}.indigo.lighten-4{background-color:#c5cae9!important}.indigo-text.text-lighten-4{color:#c5cae9!important}.indigo.lighten-3{background-color:#9fa8da!important}.indigo-text.text-lighten-3{color:#9fa8da!important}.indigo.lighten-2{background-color:#7986cb!important}.indigo-text.text-lighten-2{color:#7986cb!important}.indigo.lighten-1{background-color:#5c6bc0!important}.indigo-text.text-lighten-1{color:#5c6bc0!important}.indigo.darken-1{background-color:#3949ab!important}.indigo-text.text-darken-1{color:#3949ab!important}.indigo.darken-2{background-color:#303f9f!important}.indigo-text.text-darken-2{color:#303f9f!important}.indigo.darken-3{background-color:#283593!important}.indigo-text.text-darken-3{color:#283593!important}.indigo.darken-4{background-color:#1a237e!important}.indigo-text.text-darken-4{color:#1a237e!important}.indigo.accent-1{background-color:#8c9eff!important}.indigo-text.text-accent-1{color:#8c9eff!important}.indigo.accent-2{background-color:#536dfe!important}.indigo-text.text-accent-2{color:#536dfe!important}.indigo.accent-3{background-color:#3d5afe!important}.indigo-text.text-accent-3{color:#3d5afe!important}.indigo.accent-4{background-color:#304ffe!important}.indigo-text.text-accent-4{color:#304ffe!important}.blue{background-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue.lighten-5{background-color:#e3f2fd!important}.blue-text.text-lighten-5{color:#e3f2fd!important}.blue.lighten-4{background-color:#bbdefb!important}.blue-text.text-lighten-4{color:#bbdefb!important}.blue.lighten-3{background-color:#90caf9!important}.blue-text.text-lighten-3{color:#90caf9!important}.blue.lighten-2{background-color:#64b5f6!important}.blue-text.text-lighten-2{color:#64b5f6!important}.blue.lighten-1{background-color:#42a5f5!important}.blue-text.text-lighten-1{color:#42a5f5!important}.blue.darken-1{background-color:#1e88e5!important}.blue-text.text-darken-1{color:#1e88e5!important}.blue.darken-2{background-color:#1976d2!important}.blue-text.text-darken-2{color:#1976d2!important}.blue.darken-3{background-color:#1565c0!important}.blue-text.text-darken-3{color:#1565c0!important}.blue.darken-4{background-color:#0d47a1!important}.blue-text.text-darken-4{color:#0d47a1!important}.blue.accent-1{background-color:#82b1ff!important}.blue-text.text-accent-1{color:#82b1ff!important}.blue.accent-2{background-color:#448aff!important}.blue-text.text-accent-2{color:#448aff!important}.blue.accent-3{background-color:#2979ff!important}.blue-text.text-accent-3{color:#2979ff!important}.blue.accent-4{background-color:#2962ff!important}.blue-text.text-accent-4{color:#2962ff!important}.light-blue{background-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue.lighten-5{background-color:#e1f5fe!important}.light-blue-text.text-lighten-5{color:#e1f5fe!important}.light-blue.lighten-4{background-color:#b3e5fc!important}.light-blue-text.text-lighten-4{color:#b3e5fc!important}.light-blue.lighten-3{background-color:#81d4fa!important}.light-blue-text.text-lighten-3{color:#81d4fa!important}.light-blue.lighten-2{background-color:#4fc3f7!important}.light-blue-text.text-lighten-2{color:#4fc3f7!important}.light-blue.lighten-1{background-color:#29b6f6!important}.light-blue-text.text-lighten-1{color:#29b6f6!important}.light-blue.darken-1{background-color:#039be5!important}.light-blue-text.text-darken-1{color:#039be5!important}.light-blue.darken-2{background-color:#0288d1!important}.light-blue-text.text-darken-2{color:#0288d1!important}.light-blue.darken-3{background-color:#0277bd!important}.light-blue-text.text-darken-3{color:#0277bd!important}.light-blue.darken-4{background-color:#01579b!important}.light-blue-text.text-darken-4{color:#01579b!important}.light-blue.accent-1{background-color:#80d8ff!important}.light-blue-text.text-accent-1{color:#80d8ff!important}.light-blue.accent-2{background-color:#40c4ff!important}.light-blue-text.text-accent-2{color:#40c4ff!important}.light-blue.accent-3{background-color:#00b0ff!important}.light-blue-text.text-accent-3{color:#00b0ff!important}.light-blue.accent-4{background-color:#0091ea!important}.light-blue-text.text-accent-4{color:#0091ea!important}.cyan{background-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan.lighten-5{background-color:#e0f7fa!important}.cyan-text.text-lighten-5{color:#e0f7fa!important}.cyan.lighten-4{background-color:#b2ebf2!important}.cyan-text.text-lighten-4{color:#b2ebf2!important}.cyan.lighten-3{background-color:#80deea!important}.cyan-text.text-lighten-3{color:#80deea!important}.cyan.lighten-2{background-color:#4dd0e1!important}.cyan-text.text-lighten-2{color:#4dd0e1!important}.cyan.lighten-1{background-color:#26c6da!important}.cyan-text.text-lighten-1{color:#26c6da!important}.cyan.darken-1{background-color:#00acc1!important}.cyan-text.text-darken-1{color:#00acc1!important}.cyan.darken-2{background-color:#0097a7!important}.cyan-text.text-darken-2{color:#0097a7!important}.cyan.darken-3{background-color:#00838f!important}.cyan-text.text-darken-3{color:#00838f!important}.cyan.darken-4{background-color:#006064!important}.cyan-text.text-darken-4{color:#006064!important}.cyan.accent-1{background-color:#84ffff!important}.cyan-text.text-accent-1{color:#84ffff!important}.cyan.accent-2{background-color:#18ffff!important}.cyan-text.text-accent-2{color:#18ffff!important}.cyan.accent-3{background-color:#00e5ff!important}.cyan-text.text-accent-3{color:#00e5ff!important}.cyan.accent-4{background-color:#00b8d4!important}.cyan-text.text-accent-4{color:#00b8d4!important}.teal{background-color:#009688!important}.teal-text{color:#009688!important}.teal.lighten-5{background-color:#e0f2f1!important}.teal-text.text-lighten-5{color:#e0f2f1!important}.teal.lighten-4{background-color:#b2dfdb!important}.teal-text.text-lighten-4{color:#b2dfdb!important}.teal.lighten-3{background-color:#80cbc4!important}.teal-text.text-lighten-3{color:#80cbc4!important}.teal.lighten-2{background-color:#4db6ac!important}.teal-text.text-lighten-2{color:#4db6ac!important}.teal.lighten-1{background-color:#26a69a!important}.teal-text.text-lighten-1{color:#26a69a!important}.teal.darken-1{background-color:#00897b!important}.teal-text.text-darken-1{color:#00897b!important}.teal.darken-2{background-color:#00796b!important}.teal-text.text-darken-2{color:#00796b!important}.teal.darken-3{background-color:#00695c!important}.teal-text.text-darken-3{color:#00695c!important}.teal.darken-4{background-color:#004d40!important}.teal-text.text-darken-4{color:#004d40!important}.teal.accent-1{background-color:#a7ffeb!important}.teal-text.text-accent-1{color:#a7ffeb!important}.teal.accent-2{background-color:#64ffda!important}.teal-text.text-accent-2{color:#64ffda!important}.teal.accent-3{background-color:#1de9b6!important}.teal-text.text-accent-3{color:#1de9b6!important}.teal.accent-4{background-color:#00bfa5!important}.teal-text.text-accent-4{color:#00bfa5!important}.green{background-color:#4caf50!important}.green-text{color:#4caf50!important}.green.lighten-5{background-color:#e8f5e9!important}.green-text.text-lighten-5{color:#e8f5e9!important}.green.lighten-4{background-color:#c8e6c9!important}.green-text.text-lighten-4{color:#c8e6c9!important}.green.lighten-3{background-color:#a5d6a7!important}.green-text.text-lighten-3{color:#a5d6a7!important}.green.lighten-2{background-color:#81c784!important}.green-text.text-lighten-2{color:#81c784!important}.green.lighten-1{background-color:#66bb6a!important}.green-text.text-lighten-1{color:#66bb6a!important}.green.darken-1{background-color:#43a047!important}.green-text.text-darken-1{color:#43a047!important}.green.darken-2{background-color:#388e3c!important}.green-text.text-darken-2{color:#388e3c!important}.green.darken-3{background-color:#2e7d32!important}.green-text.text-darken-3{color:#2e7d32!important}.green.darken-4{background-color:#1b5e20!important}.green-text.text-darken-4{color:#1b5e20!important}.green.accent-1{background-color:#b9f6ca!important}.green-text.text-accent-1{color:#b9f6ca!important}.green.accent-2{background-color:#69f0ae!important}.green-text.text-accent-2{color:#69f0ae!important}.green.accent-3{background-color:#00e676!important}.green-text.text-accent-3{color:#00e676!important}.green.accent-4{background-color:#00c853!important}.green-text.text-accent-4{color:#00c853!important}.light-green{background-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green.lighten-5{background-color:#f1f8e9!important}.light-green-text.text-lighten-5{color:#f1f8e9!important}.light-green.lighten-4{background-color:#dcedc8!important}.light-green-text.text-lighten-4{color:#dcedc8!important}.light-green.lighten-3{background-color:#c5e1a5!important}.light-green-text.text-lighten-3{color:#c5e1a5!important}.light-green.lighten-2{background-color:#aed581!important}.light-green-text.text-lighten-2{color:#aed581!important}.light-green.lighten-1{background-color:#9ccc65!important}.light-green-text.text-lighten-1{color:#9ccc65!important}.light-green.darken-1{background-color:#7cb342!important}.light-green-text.text-darken-1{color:#7cb342!important}.light-green.darken-2{background-color:#689f38!important}.light-green-text.text-darken-2{color:#689f38!important}.light-green.darken-3{background-color:#558b2f!important}.light-green-text.text-darken-3{color:#558b2f!important}.light-green.darken-4{background-color:#33691e!important}.light-green-text.text-darken-4{color:#33691e!important}.light-green.accent-1{background-color:#ccff90!important}.light-green-text.text-accent-1{color:#ccff90!important}.light-green.accent-2{background-color:#b2ff59!important}.light-green-text.text-accent-2{color:#b2ff59!important}.light-green.accent-3{background-color:#76ff03!important}.light-green-text.text-accent-3{color:#76ff03!important}.light-green.accent-4{background-color:#64dd17!important}.light-green-text.text-accent-4{color:#64dd17!important}.lime{background-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime.lighten-5{background-color:#f9fbe7!important}.lime-text.text-lighten-5{color:#f9fbe7!important}.lime.lighten-4{background-color:#f0f4c3!important}.lime-text.text-lighten-4{color:#f0f4c3!important}.lime.lighten-3{background-color:#e6ee9c!important}.lime-text.text-lighten-3{color:#e6ee9c!important}.lime.lighten-2{background-color:#dce775!important}.lime-text.text-lighten-2{color:#dce775!important}.lime.lighten-1{background-color:#d4e157!important}.lime-text.text-lighten-1{color:#d4e157!important}.lime.darken-1{background-color:#c0ca33!important}.lime-text.text-darken-1{color:#c0ca33!important}.lime.darken-2{background-color:#afb42b!important}.lime-text.text-darken-2{color:#afb42b!important}.lime.darken-3{background-color:#9e9d24!important}.lime-text.text-darken-3{color:#9e9d24!important}.lime.darken-4{background-color:#827717!important}.lime-text.text-darken-4{color:#827717!important}.lime.accent-1{background-color:#f4ff81!important}.lime-text.text-accent-1{color:#f4ff81!important}.lime.accent-2{background-color:#eeff41!important}.lime-text.text-accent-2{color:#eeff41!important}.lime.accent-3{background-color:#c6ff00!important}.lime-text.text-accent-3{color:#c6ff00!important}.lime.accent-4{background-color:#aeea00!important}.lime-text.text-accent-4{color:#aeea00!important}.yellow{background-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow.lighten-5{background-color:#fffde7!important}.yellow-text.text-lighten-5{color:#fffde7!important}.yellow.lighten-4{background-color:#fff9c4!important}.yellow-text.text-lighten-4{color:#fff9c4!important}.yellow.lighten-3{background-color:#fff59d!important}.yellow-text.text-lighten-3{color:#fff59d!important}.yellow.lighten-2{background-color:#fff176!important}.yellow-text.text-lighten-2{color:#fff176!important}.yellow.lighten-1{background-color:#ffee58!important}.yellow-text.text-lighten-1{color:#ffee58!important}.yellow.darken-1{background-color:#fdd835!important}.yellow-text.text-darken-1{color:#fdd835!important}.yellow.darken-2{background-color:#fbc02d!important}.yellow-text.text-darken-2{color:#fbc02d!important}.yellow.darken-3{background-color:#f9a825!important}.yellow-text.text-darken-3{color:#f9a825!important}.yellow.darken-4{background-color:#f57f17!important}.yellow-text.text-darken-4{color:#f57f17!important}.yellow.accent-1{background-color:#ffff8d!important}.yellow-text.text-accent-1{color:#ffff8d!important}.yellow.accent-2{background-color:#ff0!important}.yellow-text.text-accent-2{color:#ff0!important}.yellow.accent-3{background-color:#ffea00!important}.yellow-text.text-accent-3{color:#ffea00!important}.yellow.accent-4{background-color:#ffd600!important}.yellow-text.text-accent-4{color:#ffd600!important}.amber{background-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber.lighten-5{background-color:#fff8e1!important}.amber-text.text-lighten-5{color:#fff8e1!important}.amber.lighten-4{background-color:#ffecb3!important}.amber-text.text-lighten-4{color:#ffecb3!important}.amber.lighten-3{background-color:#ffe082!important}.amber-text.text-lighten-3{color:#ffe082!important}.amber.lighten-2{background-color:#ffd54f!important}.amber-text.text-lighten-2{color:#ffd54f!important}.amber.lighten-1{background-color:#ffca28!important}.amber-text.text-lighten-1{color:#ffca28!important}.amber.darken-1{background-color:#ffb300!important}.amber-text.text-darken-1{color:#ffb300!important}.amber.darken-2{background-color:#ffa000!important}.amber-text.text-darken-2{color:#ffa000!important}.amber.darken-3{background-color:#ff8f00!important}.amber-text.text-darken-3{color:#ff8f00!important}.amber.darken-4{background-color:#ff6f00!important}.amber-text.text-darken-4{color:#ff6f00!important}.amber.accent-1{background-color:#ffe57f!important}.amber-text.text-accent-1{color:#ffe57f!important}.amber.accent-2{background-color:#ffd740!important}.amber-text.text-accent-2{color:#ffd740!important}.amber.accent-3{background-color:#ffc400!important}.amber-text.text-accent-3{color:#ffc400!important}.amber.accent-4{background-color:#ffab00!important}.amber-text.text-accent-4{color:#ffab00!important}.orange{background-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange.lighten-5{background-color:#fff3e0!important}.orange-text.text-lighten-5{color:#fff3e0!important}.orange.lighten-4{background-color:#ffe0b2!important}.orange-text.text-lighten-4{color:#ffe0b2!important}.orange.lighten-3{background-color:#ffcc80!important}.orange-text.text-lighten-3{color:#ffcc80!important}.orange.lighten-2{background-color:#ffb74d!important}.orange-text.text-lighten-2{color:#ffb74d!important}.orange.lighten-1{background-color:#ffa726!important}.orange-text.text-lighten-1{color:#ffa726!important}.orange.darken-1{background-color:#fb8c00!important}.orange-text.text-darken-1{color:#fb8c00!important}.orange.darken-2{background-color:#f57c00!important}.orange-text.text-darken-2{color:#f57c00!important}.orange.darken-3{background-color:#ef6c00!important}.orange-text.text-darken-3{color:#ef6c00!important}.orange.darken-4{background-color:#e65100!important}.orange-text.text-darken-4{color:#e65100!important}.orange.accent-1{background-color:#ffd180!important}.orange-text.text-accent-1{color:#ffd180!important}.orange.accent-2{background-color:#ffab40!important}.orange-text.text-accent-2{color:#ffab40!important}.orange.accent-3{background-color:#ff9100!important}.orange-text.text-accent-3{color:#ff9100!important}.orange.accent-4{background-color:#ff6d00!important}.orange-text.text-accent-4{color:#ff6d00!important}.deep-orange{background-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange.lighten-5{background-color:#fbe9e7!important}.deep-orange-text.text-lighten-5{color:#fbe9e7!important}.deep-orange.lighten-4{background-color:#ffccbc!important}.deep-orange-text.text-lighten-4{color:#ffccbc!important}.deep-orange.lighten-3{background-color:#ffab91!important}.deep-orange-text.text-lighten-3{color:#ffab91!important}.deep-orange.lighten-2{background-color:#ff8a65!important}.deep-orange-text.text-lighten-2{color:#ff8a65!important}.deep-orange.lighten-1{background-color:#ff7043!important}.deep-orange-text.text-lighten-1{color:#ff7043!important}.deep-orange.darken-1{background-color:#f4511e!important}.deep-orange-text.text-darken-1{color:#f4511e!important}.deep-orange.darken-2{background-color:#e64a19!important}.deep-orange-text.text-darken-2{color:#e64a19!important}.deep-orange.darken-3{background-color:#d84315!important}.deep-orange-text.text-darken-3{color:#d84315!important}.deep-orange.darken-4{background-color:#bf360c!important}.deep-orange-text.text-darken-4{color:#bf360c!important}.deep-orange.accent-1{background-color:#ff9e80!important}.deep-orange-text.text-accent-1{color:#ff9e80!important}.deep-orange.accent-2{background-color:#ff6e40!important}.deep-orange-text.text-accent-2{color:#ff6e40!important}.deep-orange.accent-3{background-color:#ff3d00!important}.deep-orange-text.text-accent-3{color:#ff3d00!important}.deep-orange.accent-4{background-color:#dd2c00!important}.deep-orange-text.text-accent-4{color:#dd2c00!important}.brown{background-color:#795548!important}.brown-text{color:#795548!important}.brown.lighten-5{background-color:#efebe9!important}.brown-text.text-lighten-5{color:#efebe9!important}.brown.lighten-4{background-color:#d7ccc8!important}.brown-text.text-lighten-4{color:#d7ccc8!important}.brown.lighten-3{background-color:#bcaaa4!important}.brown-text.text-lighten-3{color:#bcaaa4!important}.brown.lighten-2{background-color:#a1887f!important}.brown-text.text-lighten-2{color:#a1887f!important}.brown.lighten-1{background-color:#8d6e63!important}.brown-text.text-lighten-1{color:#8d6e63!important}.brown.darken-1{background-color:#6d4c41!important}.brown-text.text-darken-1{color:#6d4c41!important}.brown.darken-2{background-color:#5d4037!important}.brown-text.text-darken-2{color:#5d4037!important}.brown.darken-3{background-color:#4e342e!important}.brown-text.text-darken-3{color:#4e342e!important}.brown.darken-4{background-color:#3e2723!important}.brown-text.text-darken-4{color:#3e2723!important}.blue-grey{background-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey.lighten-5{background-color:#eceff1!important}.blue-grey-text.text-lighten-5{color:#eceff1!important}.blue-grey.lighten-4{background-color:#cfd8dc!important}.blue-grey-text.text-lighten-4{color:#cfd8dc!important}.blue-grey.lighten-3{background-color:#b0bec5!important}.blue-grey-text.text-lighten-3{color:#b0bec5!important}.blue-grey.lighten-2{background-color:#90a4ae!important}.blue-grey-text.text-lighten-2{color:#90a4ae!important}.blue-grey.lighten-1{background-color:#78909c!important}.blue-grey-text.text-lighten-1{color:#78909c!important}.blue-grey.darken-1{background-color:#546e7a!important}.blue-grey-text.text-darken-1{color:#546e7a!important}.blue-grey.darken-2{background-color:#455a64!important}.blue-grey-text.text-darken-2{color:#455a64!important}.blue-grey.darken-3{background-color:#37474f!important}.blue-grey-text.text-darken-3{color:#37474f!important}.blue-grey.darken-4{background-color:#263238!important}.blue-grey-text.text-darken-4{color:#263238!important}.grey{background-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey.lighten-5{background-color:#fafafa!important}.grey-text.text-lighten-5{color:#fafafa!important}.grey.lighten-4{background-color:#f5f5f5!important}.grey-text.text-lighten-4{color:#f5f5f5!important}.grey.lighten-3{background-color:#eee!important}.grey-text.text-lighten-3{color:#eee!important}.grey.lighten-2{background-color:#e0e0e0!important}.grey-text.text-lighten-2{color:#e0e0e0!important}.grey.lighten-1{background-color:#bdbdbd!important}.grey-text.text-lighten-1{color:#bdbdbd!important}.grey.darken-1{background-color:#757575!important}.grey-text.text-darken-1{color:#757575!important}.grey.darken-2{background-color:#616161!important}.grey-text.text-darken-2{color:#616161!important}.grey.darken-3{background-color:#424242!important}.grey-text.text-darken-3{color:#424242!important}.grey.darken-4{background-color:#212121!important}.grey-text.text-darken-4{color:#212121!important}.black{background-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-text{color:#fff!important}.transparent{background-color:transparent!important}.transparent-text{color:transparent!important}
-
-/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:flex;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none!important}.btn,.btn-floating,.btn-large,.btn-small,.card,.card-panel,.collapsible,.dropdown-content,.sidenav,.toast,.z-depth-1,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.12),0 1px 5px 0 rgba(0,0,0,.2)}.btn-floating:hover,.btn-large:hover,.btn-small:hover,.btn:hover,.z-depth-1-half{box-shadow:0 3px 3px 0 rgba(0,0,0,.14),0 1px 7px 0 rgba(0,0,0,.12),0 3px 1px -1px rgba(0,0,0,.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.3)}.z-depth-3{box-shadow:0 8px 17px 2px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.z-depth-4{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -7px rgba(0,0,0,.2)}.modal,.z-depth-5{box-shadow:0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12),0 11px 15px -7px rgba(0,0,0,.2)}.hoverable{transition:box-shadow .25s}.hoverable:hover{box-shadow:0 8px 17px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #b9151b}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#b9151b}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width:992px){.pagination{width:100%}.pagination li.next,.pagination li.prev{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:hsla(0,0%,100%,.7)}.breadcrumb [class*=mdi-],.breadcrumb [class^=mdi-],.breadcrumb i,.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:"\E5CC";color:hsla(0,0%,100%,.7);vertical-align:top;display:inline-block;font-family:Material Icons;font-weight:400;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-bottom,.pin-top{position:relative}.pinned{position:fixed!important}.fade-in,ul.staggered-list li{opacity:0}.fade-in{-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width:600px){.hide-on-small-and-down,.hide-on-small-only{display:none!important}}@media only screen and (max-width:992px){.hide-on-med-and-down{display:none!important}}@media only screen and (min-width:601px){.hide-on-med-and-up{display:none!important}}@media only screen and (min-width:600px) and (max-width:992px){.hide-on-med-only{display:none!important}}@media only screen and (min-width:993px){.hide-on-large-only{display:none!important}}@media only screen and (min-width:1201px){.hide-on-extra-large-only{display:none!important}}@media only screen and (min-width:1201px){.show-on-extra-large{display:block!important}}@media only screen and (min-width:993px){.show-on-large{display:block!important}}@media only screen and (min-width:600px) and (max-width:992px){.show-on-medium{display:block!important}}@media only screen and (max-width:600px){.show-on-small{display:block!important}}@media only screen and (min-width:601px){.show-on-medium-and-up{display:block!important}}@media only screen and (max-width:992px){.show-on-medium-and-down{display:block!important}}@media only screen and (max-width:600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#b9151b}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:flex;align-items:center;justify-content:space-between;padding:10px 0;color:hsla(0,0%,100%,.8);background-color:rgba(51,51,51,.08)}table,td,th{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:#212121}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:#212121}table.centered tbody tr td,table.centered thead tr th{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width:992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:"\00a0"}table.responsive-table td,table.responsive-table th{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th:before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,.12)}}.collection{margin:.5rem 0 1rem;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#0097a7;color:#c1f9ff}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#006064}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#0097a7}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#74f2ff;border-radius:2px;margin:.5rem 0 1rem;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;transition:width .3s linear}.progress .determinate,.progress .indeterminate{background-color:#0097a7}.progress .indeterminate:before{-webkit-animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite;animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}.progress .indeterminate:after,.progress .indeterminate:before{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right}.progress .indeterminate:after{-webkit-animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}.hide{display:none!important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left!important}.right{float:right!important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0!important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:.8rem;color:#fff;background-color:#0097a7;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]:after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:13px}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:"liga";font-feature-settings:"liga"}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width:601px){.container{width:85%}}@media only screen and (min-width:993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*=pull-],.row .col[class*=push-]{position:relative}.row .col.s1{width:8.33333%}.row .col.s1,.row .col.s2{margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.66667%}.row .col.s3{width:25%}.row .col.s3,.row .col.s4{margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.33333%}.row .col.s5{width:41.66667%}.row .col.s5,.row .col.s6{margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%}.row .col.s7{width:58.33333%}.row .col.s7,.row .col.s8{margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.66667%}.row .col.s9{width:75%}.row .col.s9,.row .col.s10{margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.33333%}.row .col.s11{width:91.66667%}.row .col.s11,.row .col.s12{margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%}.row .col.offset-s1{margin-left:8.33333%}.row .col.pull-s1{right:8.33333%}.row .col.push-s1{left:8.33333%}.row .col.offset-s2{margin-left:16.66667%}.row .col.pull-s2{right:16.66667%}.row .col.push-s2{left:16.66667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.33333%}.row .col.pull-s4{right:33.33333%}.row .col.push-s4{left:33.33333%}.row .col.offset-s5{margin-left:41.66667%}.row .col.pull-s5{right:41.66667%}.row .col.push-s5{left:41.66667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.33333%}.row .col.pull-s7{right:58.33333%}.row .col.push-s7{left:58.33333%}.row .col.offset-s8{margin-left:66.66667%}.row .col.pull-s8{right:66.66667%}.row .col.push-s8{left:66.66667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.33333%}.row .col.pull-s10{right:83.33333%}.row .col.push-s10{left:83.33333%}.row .col.offset-s11{margin-left:91.66667%}.row .col.pull-s11{right:91.66667%}.row .col.push-s11{left:91.66667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width:601px){.row .col.m1{width:8.33333%}.row .col.m1,.row .col.m2{margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.66667%}.row .col.m3{width:25%}.row .col.m3,.row .col.m4{margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.33333%}.row .col.m5{width:41.66667%}.row .col.m5,.row .col.m6{margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%}.row .col.m7{width:58.33333%}.row .col.m7,.row .col.m8{margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.66667%}.row .col.m9{width:75%}.row .col.m9,.row .col.m10{margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.33333%}.row .col.m11{width:91.66667%}.row .col.m11,.row .col.m12{margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%}.row .col.offset-m1{margin-left:8.33333%}.row .col.pull-m1{right:8.33333%}.row .col.push-m1{left:8.33333%}.row .col.offset-m2{margin-left:16.66667%}.row .col.pull-m2{right:16.66667%}.row .col.push-m2{left:16.66667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.33333%}.row .col.pull-m4{right:33.33333%}.row .col.push-m4{left:33.33333%}.row .col.offset-m5{margin-left:41.66667%}.row .col.pull-m5{right:41.66667%}.row .col.push-m5{left:41.66667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.33333%}.row .col.pull-m7{right:58.33333%}.row .col.push-m7{left:58.33333%}.row .col.offset-m8{margin-left:66.66667%}.row .col.pull-m8{right:66.66667%}.row .col.push-m8{left:66.66667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.33333%}.row .col.pull-m10{right:83.33333%}.row .col.push-m10{left:83.33333%}.row .col.offset-m11{margin-left:91.66667%}.row .col.pull-m11{right:91.66667%}.row .col.push-m11{left:91.66667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width:993px){.row .col.l1{width:8.33333%}.row .col.l1,.row .col.l2{margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.66667%}.row .col.l3{width:25%}.row .col.l3,.row .col.l4{margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.33333%}.row .col.l5{width:41.66667%}.row .col.l5,.row .col.l6{margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%}.row .col.l7{width:58.33333%}.row .col.l7,.row .col.l8{margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.66667%}.row .col.l9{width:75%}.row .col.l9,.row .col.l10{margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.33333%}.row .col.l11{width:91.66667%}.row .col.l11,.row .col.l12{margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%}.row .col.offset-l1{margin-left:8.33333%}.row .col.pull-l1{right:8.33333%}.row .col.push-l1{left:8.33333%}.row .col.offset-l2{margin-left:16.66667%}.row .col.pull-l2{right:16.66667%}.row .col.push-l2{left:16.66667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.33333%}.row .col.pull-l4{right:33.33333%}.row .col.push-l4{left:33.33333%}.row .col.offset-l5{margin-left:41.66667%}.row .col.pull-l5{right:41.66667%}.row .col.push-l5{left:41.66667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.33333%}.row .col.pull-l7{right:58.33333%}.row .col.push-l7{left:58.33333%}.row .col.offset-l8{margin-left:66.66667%}.row .col.pull-l8{right:66.66667%}.row .col.push-l8{left:66.66667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.33333%}.row .col.pull-l10{right:83.33333%}.row .col.push-l10{left:83.33333%}.row .col.offset-l11{margin-left:91.66667%}.row .col.pull-l11{right:91.66667%}.row .col.push-l11{left:91.66667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width:1201px){.row .col.xl1{width:8.33333%}.row .col.xl1,.row .col.xl2{margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.66667%}.row .col.xl3{width:25%}.row .col.xl3,.row .col.xl4{margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.33333%}.row .col.xl5{width:41.66667%}.row .col.xl5,.row .col.xl6{margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%}.row .col.xl7{width:58.33333%}.row .col.xl7,.row .col.xl8{margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.66667%}.row .col.xl9{width:75%}.row .col.xl9,.row .col.xl10{margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.33333%}.row .col.xl11{width:91.66667%}.row .col.xl11,.row .col.xl12{margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%}.row .col.offset-xl1{margin-left:8.33333%}.row .col.pull-xl1{right:8.33333%}.row .col.push-xl1{left:8.33333%}.row .col.offset-xl2{margin-left:16.66667%}.row .col.pull-xl2{right:16.66667%}.row .col.push-xl2{left:16.66667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.33333%}.row .col.pull-xl4{right:33.33333%}.row .col.push-xl4{left:33.33333%}.row .col.offset-xl5{margin-left:41.66667%}.row .col.pull-xl5{right:41.66667%}.row .col.push-xl5{left:41.66667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.33333%}.row .col.pull-xl7{right:58.33333%}.row .col.push-xl7{left:58.33333%}.row .col.offset-xl8{margin-left:66.66667%}.row .col.pull-xl8{right:66.66667%}.row .col.push-xl8{left:66.66667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.33333%}.row .col.pull-xl10{right:83.33333%}.row .col.push-xl10{left:83.33333%}.row .col.offset-xl11{margin-left:91.66667%}.row .col.pull-xl11{right:91.66667%}.row .col.push-xl11{left:91.66667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#b9151b;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav [class*=mdi-],nav [class^=mdi-],nav i,nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width:993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width:992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:.5rem}nav .brand-logo.right{right:.5rem;left:auto}}nav .brand-logo.right{right:.5rem;padding:0}nav .brand-logo [class*=mdi-],nav .brand-logo [class^=mdi-],nav .brand-logo i,nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-flat,nav ul a.btn-floating,nav ul a.btn-large,nav ul a.btn-small{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=date]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=text]:valid,nav .input-field input[type=url]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:hsla(0,0%,100%,.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width:601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-weight:400;color:#fff}@media only screen and (min-width:0){html{font-size:14px}}@media only screen and (min-width:992px){html{font-size:14.5px}}@media only screen and (min-width:1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;margin:2.8rem 0 1.68rem}h1,h2{line-height:110%}h2{font-size:3.56rem;margin:2.37333rem 0 1.424rem}h3{font-size:2.92rem;margin:1.94667rem 0 1.168rem}h3,h4{line-height:110%}h4{font-size:2.28rem;margin:1.52rem 0 .912rem}h5{font-size:1.64rem;margin:1.09333rem 0 .656rem}h5,h6{line-height:110%}h6{font-size:1.15rem;margin:.76667rem 0 .46rem}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width:360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width:390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width:420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width:450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width:480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width:510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width:540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width:570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width:600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width:630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width:660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width:690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width:720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width:750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width:780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width:810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width:840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width:870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width:900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width:930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width:960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width:360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63),-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .2s!important;transition:transform .2s!important;transition:transform .2s,-webkit-transform .2s!important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{padding:24px}.card,.card-panel{transition:box-shadow .25s;margin:.5rem 0 1rem;border-radius:2px;background-color:#212121}.card{position:relative}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.large,.card.medium,.card.small{position:relative}.card.large .card-image,.card.medium .card-image,.card.small .card-image{max-height:60%;overflow:hidden}.card.large .card-image+.card-content,.card.medium .card-image+.card-content,.card.small .card-image+.card-content{max-height:40%}.card.large .card-content,.card.medium .card-content,.card.small .card-content{max-height:100%;overflow:hidden}.card.large .card-action,.card.medium .card-action,.card.small .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:flex}.card.horizontal.large .card-image,.card.horizontal.medium .card-image,.card.horizontal.small .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.large .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.small .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:flex;flex-direction:column;flex:1;position:relative}.card.horizontal .card-stacked .card-content{flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#212121;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid hsla(0,0%,62.7%,.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#039be5;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#51c5fd}.card .card-reveal{padding:24px;position:absolute;background-color:#212121;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width:600px){#toast-container{min-width:100%;bottom:0}}@media only screen and (min-width:601px) and (max-width:992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width:993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:flex;align-items:center;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width:600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover,.tabs.tabs-transparent .tab a{color:hsla(0,0%,100%,.7)}.tabs.tabs-transparent .tab a.active,.tabs.tabs-transparent .tab a:hover{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:flex}.tabs.tabs-fixed-width .tab{flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(185,21,27,.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease,background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(232,51,57,.2);outline:none}.tabs .tab a.active,.tabs .tab a:hover{background-color:transparent;color:#b9151b}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(185,21,27,.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#e83339;will-change:left,right}@media only screen and (max-width:992px){.tabs{display:flex}.tabs .tab{flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none}.backdrop,.material-tooltip{opacity:0;position:absolute;visibility:hidden;background-color:#323232}.backdrop{height:7px;width:14px;border-radius:0 0 50% 50%;z-index:-1;-webkit-transform-origin:50% 0;transform-origin:50% 0}.btn,.btn-flat,.btn-large,.btn-small{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn-flat.disabled,.btn-flat:disabled,.btn-flat[disabled],.btn-floating.disabled,.btn-floating:disabled,.btn-floating[disabled],.btn-large.disabled,.btn-large:disabled,.btn-large[disabled],.btn-small.disabled,.btn-small:disabled,.btn-small[disabled],.btn.disabled,.btn:disabled,.btn[disabled],.disabled.btn-large,.disabled.btn-small{pointer-events:none;background-color:#dfdfdf!important;box-shadow:none;color:#9f9f9f!important;cursor:default}.btn-flat.disabled:hover,.btn-flat:disabled:hover,.btn-flat[disabled]:hover,.btn-floating.disabled:hover,.btn-floating:disabled:hover,.btn-floating[disabled]:hover,.btn-large.disabled:hover,.btn-large:disabled:hover,.btn-large[disabled]:hover,.btn-small.disabled:hover,.btn-small:disabled:hover,.btn-small[disabled]:hover,.btn.disabled:hover,.btn:disabled:hover,.btn[disabled]:hover,.disabled.btn-large:hover,.disabled.btn-small:hover{background-color:#dfdfdf!important;color:#9f9f9f!important}.btn,.btn-flat,.btn-floating,.btn-large,.btn-small{font-size:14px;outline:0}.btn-flat i,.btn-floating i,.btn-large i,.btn-small i,.btn i{font-size:1.3rem;line-height:inherit}.btn-floating:focus,.btn-large:focus,.btn-small:focus,.btn:focus{background-color:#006974}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#0097a7;text-align:center;letter-spacing:.5px;transition:background-color .2s ease-out;cursor:pointer}.btn-large:hover,.btn-small:hover,.btn:hover{background-color:#00aec1}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;border-radius:50%;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating,.btn-floating:hover{background-color:#0097a7}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:flex;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#0097a7;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{background-color:transparent;color:#fff;cursor:pointer;transition:background-color .2s}.btn-flat,.btn-flat:focus,.btn-flat:hover{box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,.1)}.btn-flat.btn-flat[disabled],.btn-flat.disabled{background-color:transparent!important;color:#b3b3b3!important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:#fff;cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li.active,.dropdown-content li:hover{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#0097a7;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type=checkbox]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}
-
-/*!
- * Waves v0.6.0
- * http://fian.my.id/Waves
- *
- * Copyright 2014 Alfiana E. Sibuea and other contributors
- * Released under the MIT license
- * https://github.com/fians/Waves/blob/master/LICENSE
- */.dropdown-trigger,.waves-effect{cursor:pointer}.waves-effect{position:relative;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.2);transition:all .7s ease-out;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:hsla(0,0%,100%,.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,.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{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle,#fff 100%,#000 0)}.waves-input-wrapper{border-radius:.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-flat,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small{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,.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}.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,.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,.16),0 2px 10px 0 rgba(0,0,0,.12);margin:0 24px;transition:margin .35s cubic-bezier(.25,.46,.45,.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(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;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,.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:.8rem;-webkit-transform:translateY(-140%);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{top:0;right:0;background-color:#292929;will-change:opacity}#materialbox-overlay,.materialbox-caption{position:fixed;bottom:0;left:0;z-index:1000}.materialbox-caption{display:none;color:#fff;line-height:50px;width:100%;text-align:center;padding:0 15%;height:50px;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #98f5ff}button:focus{outline:none;background-color:#00a9bb}label{font-size:.8rem;color:#fff}::-webkit-input-placeholder{color:#fff}::-moz-placeholder{color:#fff}:-ms-input-placeholder{color:#fff}::-ms-input-placeholder{color:#fff}::placeholder{color:#fff}input:not([type]),input[type=date]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=email]:not(.browser-default),input[type=number]:not(.browser-default),input[type=password]:not(.browser-default),input[type=search]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=text]:not(.browser-default),input[type=time]:not(.browser-default),input[type=url]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #fff;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px;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=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly=readonly],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly=readonly],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly=readonly],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly=readonly],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly=readonly],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly=readonly],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly=readonly],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly=readonly],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly=readonly],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly=readonly],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly=readonly],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly=readonly]{color:rgba(0,0,0,.42);border-bottom:1px dotted rgba(0,0,0,.42)}input:not([type]):disabled+label,input:not([type])[readonly=readonly]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]: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=datetime]:not(.browser-default):disabled+label,input[type=datetime]: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=number]:not(.browser-default):disabled+label,input[type=number]: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=search]:not(.browser-default):disabled+label,input[type=search]: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=text]:not(.browser-default):disabled+label,input[type=text]: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=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly=readonly]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly=readonly]+label{color:rgba(0,0,0,.42)}input:not([type]):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=url]: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=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#0097a7}input:not([type]):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#388e3c}input:not([type]):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#d32f2f}input:not([type]).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=url]: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.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]: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.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]: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.invalid~.helper-text[data-error],.select-wrapper.valid .helper-text[data-success],input:not([type]).invalid~.helper-text[data-error],input:not([type]).valid~.helper-text[data-success],input:not([type]):focus.invalid~.helper-text[data-error],input:not([type]):focus.valid~.helper-text[data-success],input[type=date]:not(.browser-default).invalid~.helper-text[data-error],input[type=date]:not(.browser-default).valid~.helper-text[data-success],input[type=date]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=date]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid~.helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid~.helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=datetime]:not(.browser-default).invalid~.helper-text[data-error],input[type=datetime]:not(.browser-default).valid~.helper-text[data-success],input[type=datetime]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=datetime]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=email]:not(.browser-default).invalid~.helper-text[data-error],input[type=email]:not(.browser-default).valid~.helper-text[data-success],input[type=email]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=email]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=number]:not(.browser-default).invalid~.helper-text[data-error],input[type=number]:not(.browser-default).valid~.helper-text[data-success],input[type=number]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=number]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=password]:not(.browser-default).invalid~.helper-text[data-error],input[type=password]:not(.browser-default).valid~.helper-text[data-success],input[type=password]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=password]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=search]:not(.browser-default).invalid~.helper-text[data-error],input[type=search]:not(.browser-default).valid~.helper-text[data-success],input[type=search]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=search]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=tel]:not(.browser-default).invalid~.helper-text[data-error],input[type=tel]:not(.browser-default).valid~.helper-text[data-success],input[type=tel]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=tel]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=text]:not(.browser-default).invalid~.helper-text[data-error],input[type=text]:not(.browser-default).valid~.helper-text[data-success],input[type=text]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=text]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=time]:not(.browser-default).invalid~.helper-text[data-error],input[type=time]:not(.browser-default).valid~.helper-text[data-success],input[type=time]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=time]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=url]:not(.browser-default).invalid~.helper-text[data-error],input[type=url]:not(.browser-default).valid~.helper-text[data-success],input[type=url]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=url]:not(.browser-default):focus.valid~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea:focus.invalid~.helper-text[data-error],textarea.materialize-textarea:focus.valid~.helper-text[data-success]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input:not([type]).valid~.helper-text:after,input:not([type]):focus.valid~.helper-text:after,input[type=date]:not(.browser-default).valid~.helper-text:after,input[type=date]:not(.browser-default):focus.valid~.helper-text:after,input[type=datetime-local]:not(.browser-default).valid~.helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid~.helper-text:after,input[type=datetime]:not(.browser-default).valid~.helper-text:after,input[type=datetime]:not(.browser-default):focus.valid~.helper-text:after,input[type=email]:not(.browser-default).valid~.helper-text:after,input[type=email]:not(.browser-default):focus.valid~.helper-text:after,input[type=number]:not(.browser-default).valid~.helper-text:after,input[type=number]:not(.browser-default):focus.valid~.helper-text:after,input[type=password]:not(.browser-default).valid~.helper-text:after,input[type=password]:not(.browser-default):focus.valid~.helper-text:after,input[type=search]:not(.browser-default).valid~.helper-text:after,input[type=search]:not(.browser-default):focus.valid~.helper-text:after,input[type=tel]:not(.browser-default).valid~.helper-text:after,input[type=tel]:not(.browser-default):focus.valid~.helper-text:after,input[type=text]:not(.browser-default).valid~.helper-text:after,input[type=text]:not(.browser-default):focus.valid~.helper-text:after,input[type=time]:not(.browser-default).valid~.helper-text:after,input[type=time]:not(.browser-default):focus.valid~.helper-text:after,input[type=url]:not(.browser-default).valid~.helper-text:after,input[type=url]:not(.browser-default):focus.valid~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea:focus.valid~.helper-text:after{content:attr(data-success);color:#388e3c}.select-wrapper.invalid~.helper-text:after,input:not([type]).invalid~.helper-text:after,input:not([type]):focus.invalid~.helper-text:after,input[type=date]:not(.browser-default).invalid~.helper-text:after,input[type=date]:not(.browser-default):focus.invalid~.helper-text:after,input[type=datetime-local]:not(.browser-default).invalid~.helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid~.helper-text:after,input[type=datetime]:not(.browser-default).invalid~.helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid~.helper-text:after,input[type=email]:not(.browser-default).invalid~.helper-text:after,input[type=email]:not(.browser-default):focus.invalid~.helper-text:after,input[type=number]:not(.browser-default).invalid~.helper-text:after,input[type=number]:not(.browser-default):focus.invalid~.helper-text:after,input[type=password]:not(.browser-default).invalid~.helper-text:after,input[type=password]:not(.browser-default):focus.invalid~.helper-text:after,input[type=search]:not(.browser-default).invalid~.helper-text:after,input[type=search]:not(.browser-default):focus.invalid~.helper-text:after,input[type=tel]:not(.browser-default).invalid~.helper-text:after,input[type=tel]:not(.browser-default):focus.invalid~.helper-text:after,input[type=text]:not(.browser-default).invalid~.helper-text:after,input[type=text]:not(.browser-default):focus.invalid~.helper-text:after,input[type=time]:not(.browser-default).invalid~.helper-text:after,input[type=time]:not(.browser-default):focus.invalid~.helper-text:after,input[type=url]:not(.browser-default).invalid~.helper-text:after,input[type=url]:not(.browser-default):focus.invalid~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea:focus.invalid~.helper-text:after{content:attr(data-error);color:#d32f2f}.select-wrapper+label:after,input:not([type])+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=text]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:opacity .2s ease-out,color .2s 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 .select-dropdown,.input-field.inline input{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~.validate~label,.input-field.col .prefix~label{width:calc(100% - 4.5rem)}.input-field>label{color:#fff;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:color .2s ease-out,-webkit-transform .2s ease-out;transition:transform .2s ease-out,color .2s ease-out;transition:transform .2s ease-out,color .2s ease-out,-webkit-transform .2s ease-out;-webkit-transform-origin:0 100%;transform-origin:0 100%;text-align:left;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(.8);transform:translateY(-14px) scale(.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label,.input-field>input[type]:-webkit-autofill:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(.8);transform:translateY(-14px) scale(.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(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~.autocomplete-content,.input-field .prefix~.helper-text,.input-field .prefix~.validate~label,.input-field .prefix~input,.input-field .prefix~label,.input-field .prefix~textarea{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:background-color .3s}.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)~.material-icons,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search]~.material-icons,.input-field input[type=search]~.mdi-navigation-close{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-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]:checked,[type=radio]:not(:checked){position:absolute;opacity:0;pointer-events:none}[type=radio]:checked+span,[type=radio]:not(:checked)+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type=radio]+span:after,[type=radio]+span:before{content:"";position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type=radio].with-gap:checked+span:after,[type=radio].with-gap:checked+span:before,[type=radio]:checked+span:after,[type=radio]:checked+span:before,[type=radio]:not(:checked)+span:after,[type=radio]:not(:checked)+span:before{border-radius:50%}[type=radio]:not(:checked)+span:after,[type=radio]:not(:checked)+span:before{border:2px solid #fff}[type=radio]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type=radio]:checked+span:before{border:2px solid transparent}[type=radio].with-gap:checked+span:after,[type=radio].with-gap:checked+span:before,[type=radio]:checked+span:after{border:2px solid #0097a7}[type=radio].with-gap:checked+span:after,[type=radio]:checked+span:after{background-color:#0097a7}[type=radio]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type=radio].with-gap:checked+span:after{-webkit-transform:scale(.5);transform:scale(.5)}[type=radio].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,.1)}[type=radio].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,.42)}[type=radio].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,.42)}[type=radio]:disabled:checked+span:before,[type=radio]:disabled:not(:checked)+span:before{background-color:transparent;border-color:rgba(0,0,0,.42)}[type=radio]:disabled+span{color:rgba(0,0,0,.42)}[type=radio]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,.42)}[type=radio]:disabled:checked+span:after{background-color:rgba(0,0,0,.42);border-color:#949494}[type=checkbox]:checked,[type=checkbox]:not(: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;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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;-webkit-transform:scale(0);transform:scale(0)}[type=checkbox]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,.42)}[type=checkbox].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}[type=checkbox]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-color:transparent #0097a7 #0097a7 transparent;border-style:solid;border-width:2px;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,.42);border-bottom:2px solid rgba(0,0,0,.42)}[type=checkbox]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border:none;border-right:2px solid #0097a7;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(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):after,[type=checkbox].filled-in+span:not(.lever):before{content:"";left:0;position:absolute;transition:border .25s,background-color .25s,width .2s .1s,height .2s .1s,top .2s .1s,left .2s .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;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;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:0;z-index:0}[type=checkbox].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-color:transparent #fff #fff transparent;border-style:solid;border-width:2px;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;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,.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;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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:after,.switch label input[type=checkbox]:checked+.lever:before{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,.38);border-radius:15px;transition:background .3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:after,.switch label .lever:before{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left .3s ease,background .3s ease,box-shadow .1s ease,-webkit-transform .1s ease;transition:left .3s ease,background .3s ease,box-shadow .1s ease,transform .1s ease;transition:left .3s ease,background .3s ease,box-shadow .1s ease,transform .1s ease,-webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(0,151,167,.15)}.switch label .lever:after{background-color:#f1f1f1;box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever:before,input[type=checkbox]:checked:not(:disabled)~.lever:active:before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,151,167,.15)}input[type=checkbox]:not(:disabled).tabbed:focus~.lever:before,input[type=checkbox]:not(:disabled)~.lever:active:before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(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:hsla(0,0%,100%,.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.invalid+label,.select-wrapper.valid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #fff;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}.select-wrapper.disabled+label,select:disabled{color:rgba(0,0,0,.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,.3);background-color:transparent}.select-dropdown.dropdown-content li:hover,body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(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,.7)}.select-dropdown li.optgroup>span{color:rgba(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;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#0097a7;font-size:0;-webkit-transform:rotate(45deg);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;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,151,167,.26)}input[type=range]{border:1px solid #fff}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,.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,.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{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 #b9151b}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #b9151b}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:calc(100% + 60px);height:100%;padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,.05)}.sidenav li>a{color:rgba(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,.05)}.sidenav li>a.btn,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating,.sidenav li>a.btn-large,.sidenav li>a.btn-small{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating,.sidenav li>a.btn-large,.sidenav li>a.btn-small{color:#fff}.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn:hover{background-color:#00aec1}.sidenav li>a.btn-floating:hover{background-color:#0097a7}.sidenav li>a>[class^=mdi-],.sidenav li>a>i,.sidenav li>a>i.material-icons,.sidenav li>a li>a>[class*=mdi-]{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,.54)}.sidenav .divider{margin:8px 0 0}.sidenav .subheader{cursor:auto;pointer-events:none;color:rgba(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 .email,.sidenav .user-view .name{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .email,.sidenav .user-view .name{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;-webkit-transform:translateX(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{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);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:#b9151b}.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,.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(1turn)}}@keyframes container-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.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(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-green-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,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(3turn)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}@-webkit-keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@-webkit-keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}@keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{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:3px 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(.4,0,.2,1) infinite both;animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes left-spin{0%{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{0%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{0%{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{0%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1);animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1)}@-webkit-keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes fade-out{0%{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:50%}.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;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0 50%;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:hsla(0,0%,100%,.5);transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel .carousel-item:not(.active) .materialboxed,.carousel.scrolling .carousel-item .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{opacity:.95;transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-wrapper.open .tap-target,.tap-target-wrapper.open .tap-target-wave:before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave:after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;transition:opacity .3s,visibility 0s 1s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s 1s;transition:opacity .3s,transform .3s,visibility 0s 1s,-webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#b9151b;box-shadow:0 20px 20px 0 rgba(0,0,0,.14),0 10px 50px 0 rgba(0,0,0,.12),0 30px 10px -20px rgba(0,0,0,.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave:after,.tap-target-wave:before{content:"";display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#fff}.tap-target-wave:before{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.tap-target-wave:after{visibility:hidden;transition:opacity .3s,visibility 0s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s;transition:opacity .3s,transform .3s,visibility 0s,-webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%,-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,-webkit-transform .3s;transition:opacity .3s,transform .3s;transition:opacity .3s,transform .3s,-webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);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-next,.month-prev{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:hsla(0,0%,100%,.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-disabled,.datepicker-table td.is-outside-current-month{color:rgba(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,.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-done,.datepicker-today{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-footer,.datepicker-table{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:700;text-align:center;color:hsla(0,0%,100%,.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-am-pm div,.timepicker-span-hours,.timepicker-span-minutes{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:25px auto 5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.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,.25)}.timepicker-dial{transition:opacity .35s,-webkit-transform .35s;transition:transform .35s,opacity .35s;transition:transform .35s,opacity .35s,-webkit-transform .35s}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1);transform:scale(1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(.8);transform:scale(.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#0097a7;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:.25}.timepicker-canvas-bearing,.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}.pagination li a,a.unmarked{color:#fff}.progress{background-color:#424242}.progress>.determinate{background-color:#2196f3}html{background-color:#101010}input[type=email],input[type=password],input[type=text],textarea{color:#fff}
+.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:flex;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none !important}.z-depth-1,.sidenav,.collapsible,.dropdown-content,.btn-floating,.btn,.btn-small,.btn-large,.toast,.card,.card-panel,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn-floating:hover,.btn:hover,.btn-small:hover,.btn-large:hover{box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{transition:box-shadow .25s}.hoverable:hover{box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #8b1014}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#8b1014}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width : 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;transform:translate3d(0, 0, 0);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;transform-origin:0 50%}@media only screen and (max-width : 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width : 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width : 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width : 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width : 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width : 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width : 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width : 600px){.show-on-small{display:block !important}}@media only screen and (min-width : 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width : 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width : 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#8b1014}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:flex;align-items:center;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:#212121}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:#212121}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width : 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #424242;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#101010;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #424242}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#0097a7;color:#c1f9ff}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#fff}.collection a.collection-item:not(.active):hover{background-color:#212121}.collection.with-header .collection-header{background-color:#101010;border-bottom:1px solid #424242;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#0097a7}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#74f2ff;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#0097a7;transition:width .3s linear}.progress .indeterminate{background-color:#0097a7}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation-delay:1.15s}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#0097a7;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width : 601px){.container{width:85%}}@media only screen and (min-width : 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width : 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width : 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width : 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#8b1014;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width : 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;transform:translateX(-50%)}@media only screen and (max-width : 992px){nav .brand-logo{left:50%;transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width : 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:#fff}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{transform:scale(0);transition:transform .2s !important}.scale-transition.scale-in{transform:scale(1)}.card-panel{transition:box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#212121}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#212121;transition:box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:flex;flex-direction:column;flex:1;position:relative}.card.horizontal .card-stacked .card-content{flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#212121;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-small):not(.btn-large):not(.btn-large):not(.btn-floating){color:#039be5;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-small):not(.btn-large):not(.btn-large):not(.btn-floating):hover{color:#51c5fd}.card .card-reveal{padding:24px;position:absolute;background-color:#212121;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width : 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width : 601px) and (max-width : 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width : 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:flex;align-items:center;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width : 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:flex}.tabs.tabs-fixed-width .tab{flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(139,16,20,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(208,24,30,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#8b1014}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(139,16,20,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#d0181e;will-change:left, right}@media only screen and (max-width : 992px){.tabs{display:flex}.tabs .tab{flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;transform-origin:50% 0%;visibility:hidden}.btn,.btn-small,.btn-large,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-small:focus,.btn-large:focus,.btn-floating:focus{background-color:#006974}.btn,.btn-small,.btn-large{text-decoration:none;color:#fff;background-color:#0097a7;text-align:center;letter-spacing:.5px;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-small:hover,.btn-large:hover{background-color:#00aec1}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#0097a7;border-radius:50%;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#0097a7}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:flex;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#0097a7;border-radius:50%;transform:scale(0)}.btn-flat{box-shadow:none;background-color:transparent;color:#fff;cursor:pointer;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b3b3 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:#fff;cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#0097a7;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;transform:none}.dropdown-trigger{cursor:pointer}/*!
+ * Waves v0.6.0
+ * http://fian.my.id/Waves
+ *
+ * Copyright 2014 Alfiana E. Sibuea and other contributors
+ * Released under the MIT license
+ * https://github.com/fians/Waves/blob/master/LICENSE
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #98f5ff}button:focus{outline:none;background-color:#00a9bb}label{font-size:.8rem;color:#fff}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #0097a7;box-shadow:0 1px 0 0 #0097a7}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#0097a7}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#388E3C}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#D32F2F}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #388E3C;box-shadow:0 1px 0 0 #388E3C}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #D32F2F;box-shadow:0 1px 0 0 #D32F2F}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#388E3C}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#D32F2F}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#fff;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#0097a7}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #fff}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #0097a7}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#0097a7}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #fff;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #0097a7;border-bottom:2px solid #0097a7;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #0097a7;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #fff;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #0097a7;background-color:#0097a7;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#fff;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#0097a7;border-color:#0097a7}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#42d5e4}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#0097a7}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(0,151,167,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,151,167,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #0097a7}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#0097a7;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#0097a7;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;-webkit-appearance:none;background-color:#0097a7;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #8b1014}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #8b1014}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#fff}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#00aec1}.sidenav li>a.btn-floating:hover{background-color:#0097a7}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#8b1014}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#0097a7}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#757575;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#8b1014;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#0097a7;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#0097a7}.datepicker-table td.is-selected{background-color:#0097a7;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(4,148,163,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#0097a7;padding:0 1rem}.datepicker-clear{color:#D32F2F}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#0097a7;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(0,151,167,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#0097a7;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#0097a7}.timepicker-canvas-bg{stroke:none;fill:#0097a7}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#D32F2F}.timepicker-close{color:#0097a7}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#D32F2F}.caution-color-text{color:#D32F2F}.info-color{background-color:#263238}.info-color-text{color:#263238}.contrast-color{background-color:#fff}.contrast-color-text{color:#fff}.success-color{background-color:#388E3C}.success-color-text{color:#388E3C}html{background-color:#101010}a.unmarked{color:#fff}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:#fff}.collection-item{color:#fff}.collection-item .secondary-content{color:#fff}.collection-item.disabled{color:#757575}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#212121;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#212121;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#702020;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:#fff}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:#fff}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.departures .dep-dest .followee-checkin i.material-icons{vertical-align:middle}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:#fff;position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.RUF,.dep-line.AST{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.STB,.dep-line.M{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}.progress{background-color:#424242}.progress>.determinate{background-color:#2196F3}html{background-color:#101010}input[type=email],input[type=text],input[type=password],textarea{color:#fff}
diff --git a/public/static/css/light.min.css b/public/static/css/light.min.css
index 97066e1..22a7b63 100644
--- a/public/static/css/light.min.css
+++ b/public/static/css/light.min.css
@@ -1,12 +1,8 @@
-.materialize-red{background-color:#e51c23!important}.materialize-red-text{color:#e51c23!important}.materialize-red.lighten-5{background-color:#fdeaeb!important}.materialize-red-text.text-lighten-5{color:#fdeaeb!important}.materialize-red.lighten-4{background-color:#f8c1c3!important}.materialize-red-text.text-lighten-4{color:#f8c1c3!important}.materialize-red.lighten-3{background-color:#f3989b!important}.materialize-red-text.text-lighten-3{color:#f3989b!important}.materialize-red.lighten-2{background-color:#ee6e73!important}.materialize-red-text.text-lighten-2{color:#ee6e73!important}.materialize-red.lighten-1{background-color:#ea454b!important}.materialize-red-text.text-lighten-1{color:#ea454b!important}.materialize-red.darken-1{background-color:#d0181e!important}.materialize-red-text.text-darken-1{color:#d0181e!important}.materialize-red.darken-2{background-color:#b9151b!important}.materialize-red-text.text-darken-2{color:#b9151b!important}.materialize-red.darken-3{background-color:#a21318!important}.materialize-red-text.text-darken-3{color:#a21318!important}.materialize-red.darken-4{background-color:#8b1014!important}.materialize-red-text.text-darken-4{color:#8b1014!important}.red{background-color:#f44336!important}.red-text{color:#f44336!important}.red.lighten-5{background-color:#ffebee!important}.red-text.text-lighten-5{color:#ffebee!important}.red.lighten-4{background-color:#ffcdd2!important}.red-text.text-lighten-4{color:#ffcdd2!important}.red.lighten-3{background-color:#ef9a9a!important}.red-text.text-lighten-3{color:#ef9a9a!important}.red.lighten-2{background-color:#e57373!important}.red-text.text-lighten-2{color:#e57373!important}.red.lighten-1{background-color:#ef5350!important}.red-text.text-lighten-1{color:#ef5350!important}.red.darken-1{background-color:#e53935!important}.red-text.text-darken-1{color:#e53935!important}.red.darken-2{background-color:#d32f2f!important}.red-text.text-darken-2{color:#d32f2f!important}.red.darken-3{background-color:#c62828!important}.red-text.text-darken-3{color:#c62828!important}.red.darken-4{background-color:#b71c1c!important}.red-text.text-darken-4{color:#b71c1c!important}.red.accent-1{background-color:#ff8a80!important}.red-text.text-accent-1{color:#ff8a80!important}.red.accent-2{background-color:#ff5252!important}.red-text.text-accent-2{color:#ff5252!important}.red.accent-3{background-color:#ff1744!important}.red-text.text-accent-3{color:#ff1744!important}.red.accent-4{background-color:#d50000!important}.red-text.text-accent-4{color:#d50000!important}.pink{background-color:#e91e63!important}.pink-text{color:#e91e63!important}.pink.lighten-5{background-color:#fce4ec!important}.pink-text.text-lighten-5{color:#fce4ec!important}.pink.lighten-4{background-color:#f8bbd0!important}.pink-text.text-lighten-4{color:#f8bbd0!important}.pink.lighten-3{background-color:#f48fb1!important}.pink-text.text-lighten-3{color:#f48fb1!important}.pink.lighten-2{background-color:#f06292!important}.pink-text.text-lighten-2{color:#f06292!important}.pink.lighten-1{background-color:#ec407a!important}.pink-text.text-lighten-1{color:#ec407a!important}.pink.darken-1{background-color:#d81b60!important}.pink-text.text-darken-1{color:#d81b60!important}.pink.darken-2{background-color:#c2185b!important}.pink-text.text-darken-2{color:#c2185b!important}.pink.darken-3{background-color:#ad1457!important}.pink-text.text-darken-3{color:#ad1457!important}.pink.darken-4{background-color:#880e4f!important}.pink-text.text-darken-4{color:#880e4f!important}.pink.accent-1{background-color:#ff80ab!important}.pink-text.text-accent-1{color:#ff80ab!important}.pink.accent-2{background-color:#ff4081!important}.pink-text.text-accent-2{color:#ff4081!important}.pink.accent-3{background-color:#f50057!important}.pink-text.text-accent-3{color:#f50057!important}.pink.accent-4{background-color:#c51162!important}.pink-text.text-accent-4{color:#c51162!important}.purple{background-color:#9c27b0!important}.purple-text{color:#9c27b0!important}.purple.lighten-5{background-color:#f3e5f5!important}.purple-text.text-lighten-5{color:#f3e5f5!important}.purple.lighten-4{background-color:#e1bee7!important}.purple-text.text-lighten-4{color:#e1bee7!important}.purple.lighten-3{background-color:#ce93d8!important}.purple-text.text-lighten-3{color:#ce93d8!important}.purple.lighten-2{background-color:#ba68c8!important}.purple-text.text-lighten-2{color:#ba68c8!important}.purple.lighten-1{background-color:#ab47bc!important}.purple-text.text-lighten-1{color:#ab47bc!important}.purple.darken-1{background-color:#8e24aa!important}.purple-text.text-darken-1{color:#8e24aa!important}.purple.darken-2{background-color:#7b1fa2!important}.purple-text.text-darken-2{color:#7b1fa2!important}.purple.darken-3{background-color:#6a1b9a!important}.purple-text.text-darken-3{color:#6a1b9a!important}.purple.darken-4{background-color:#4a148c!important}.purple-text.text-darken-4{color:#4a148c!important}.purple.accent-1{background-color:#ea80fc!important}.purple-text.text-accent-1{color:#ea80fc!important}.purple.accent-2{background-color:#e040fb!important}.purple-text.text-accent-2{color:#e040fb!important}.purple.accent-3{background-color:#d500f9!important}.purple-text.text-accent-3{color:#d500f9!important}.purple.accent-4{background-color:#a0f!important}.purple-text.text-accent-4{color:#a0f!important}.deep-purple{background-color:#673ab7!important}.deep-purple-text{color:#673ab7!important}.deep-purple.lighten-5{background-color:#ede7f6!important}.deep-purple-text.text-lighten-5{color:#ede7f6!important}.deep-purple.lighten-4{background-color:#d1c4e9!important}.deep-purple-text.text-lighten-4{color:#d1c4e9!important}.deep-purple.lighten-3{background-color:#b39ddb!important}.deep-purple-text.text-lighten-3{color:#b39ddb!important}.deep-purple.lighten-2{background-color:#9575cd!important}.deep-purple-text.text-lighten-2{color:#9575cd!important}.deep-purple.lighten-1{background-color:#7e57c2!important}.deep-purple-text.text-lighten-1{color:#7e57c2!important}.deep-purple.darken-1{background-color:#5e35b1!important}.deep-purple-text.text-darken-1{color:#5e35b1!important}.deep-purple.darken-2{background-color:#512da8!important}.deep-purple-text.text-darken-2{color:#512da8!important}.deep-purple.darken-3{background-color:#4527a0!important}.deep-purple-text.text-darken-3{color:#4527a0!important}.deep-purple.darken-4{background-color:#311b92!important}.deep-purple-text.text-darken-4{color:#311b92!important}.deep-purple.accent-1{background-color:#b388ff!important}.deep-purple-text.text-accent-1{color:#b388ff!important}.deep-purple.accent-2{background-color:#7c4dff!important}.deep-purple-text.text-accent-2{color:#7c4dff!important}.deep-purple.accent-3{background-color:#651fff!important}.deep-purple-text.text-accent-3{color:#651fff!important}.deep-purple.accent-4{background-color:#6200ea!important}.deep-purple-text.text-accent-4{color:#6200ea!important}.indigo{background-color:#3f51b5!important}.indigo-text{color:#3f51b5!important}.indigo.lighten-5{background-color:#e8eaf6!important}.indigo-text.text-lighten-5{color:#e8eaf6!important}.indigo.lighten-4{background-color:#c5cae9!important}.indigo-text.text-lighten-4{color:#c5cae9!important}.indigo.lighten-3{background-color:#9fa8da!important}.indigo-text.text-lighten-3{color:#9fa8da!important}.indigo.lighten-2{background-color:#7986cb!important}.indigo-text.text-lighten-2{color:#7986cb!important}.indigo.lighten-1{background-color:#5c6bc0!important}.indigo-text.text-lighten-1{color:#5c6bc0!important}.indigo.darken-1{background-color:#3949ab!important}.indigo-text.text-darken-1{color:#3949ab!important}.indigo.darken-2{background-color:#303f9f!important}.indigo-text.text-darken-2{color:#303f9f!important}.indigo.darken-3{background-color:#283593!important}.indigo-text.text-darken-3{color:#283593!important}.indigo.darken-4{background-color:#1a237e!important}.indigo-text.text-darken-4{color:#1a237e!important}.indigo.accent-1{background-color:#8c9eff!important}.indigo-text.text-accent-1{color:#8c9eff!important}.indigo.accent-2{background-color:#536dfe!important}.indigo-text.text-accent-2{color:#536dfe!important}.indigo.accent-3{background-color:#3d5afe!important}.indigo-text.text-accent-3{color:#3d5afe!important}.indigo.accent-4{background-color:#304ffe!important}.indigo-text.text-accent-4{color:#304ffe!important}.blue{background-color:#2196f3!important}.blue-text{color:#2196f3!important}.blue.lighten-5{background-color:#e3f2fd!important}.blue-text.text-lighten-5{color:#e3f2fd!important}.blue.lighten-4{background-color:#bbdefb!important}.blue-text.text-lighten-4{color:#bbdefb!important}.blue.lighten-3{background-color:#90caf9!important}.blue-text.text-lighten-3{color:#90caf9!important}.blue.lighten-2{background-color:#64b5f6!important}.blue-text.text-lighten-2{color:#64b5f6!important}.blue.lighten-1{background-color:#42a5f5!important}.blue-text.text-lighten-1{color:#42a5f5!important}.blue.darken-1{background-color:#1e88e5!important}.blue-text.text-darken-1{color:#1e88e5!important}.blue.darken-2{background-color:#1976d2!important}.blue-text.text-darken-2{color:#1976d2!important}.blue.darken-3{background-color:#1565c0!important}.blue-text.text-darken-3{color:#1565c0!important}.blue.darken-4{background-color:#0d47a1!important}.blue-text.text-darken-4{color:#0d47a1!important}.blue.accent-1{background-color:#82b1ff!important}.blue-text.text-accent-1{color:#82b1ff!important}.blue.accent-2{background-color:#448aff!important}.blue-text.text-accent-2{color:#448aff!important}.blue.accent-3{background-color:#2979ff!important}.blue-text.text-accent-3{color:#2979ff!important}.blue.accent-4{background-color:#2962ff!important}.blue-text.text-accent-4{color:#2962ff!important}.light-blue{background-color:#03a9f4!important}.light-blue-text{color:#03a9f4!important}.light-blue.lighten-5{background-color:#e1f5fe!important}.light-blue-text.text-lighten-5{color:#e1f5fe!important}.light-blue.lighten-4{background-color:#b3e5fc!important}.light-blue-text.text-lighten-4{color:#b3e5fc!important}.light-blue.lighten-3{background-color:#81d4fa!important}.light-blue-text.text-lighten-3{color:#81d4fa!important}.light-blue.lighten-2{background-color:#4fc3f7!important}.light-blue-text.text-lighten-2{color:#4fc3f7!important}.light-blue.lighten-1{background-color:#29b6f6!important}.light-blue-text.text-lighten-1{color:#29b6f6!important}.light-blue.darken-1{background-color:#039be5!important}.light-blue-text.text-darken-1{color:#039be5!important}.light-blue.darken-2{background-color:#0288d1!important}.light-blue-text.text-darken-2{color:#0288d1!important}.light-blue.darken-3{background-color:#0277bd!important}.light-blue-text.text-darken-3{color:#0277bd!important}.light-blue.darken-4{background-color:#01579b!important}.light-blue-text.text-darken-4{color:#01579b!important}.light-blue.accent-1{background-color:#80d8ff!important}.light-blue-text.text-accent-1{color:#80d8ff!important}.light-blue.accent-2{background-color:#40c4ff!important}.light-blue-text.text-accent-2{color:#40c4ff!important}.light-blue.accent-3{background-color:#00b0ff!important}.light-blue-text.text-accent-3{color:#00b0ff!important}.light-blue.accent-4{background-color:#0091ea!important}.light-blue-text.text-accent-4{color:#0091ea!important}.cyan{background-color:#00bcd4!important}.cyan-text{color:#00bcd4!important}.cyan.lighten-5{background-color:#e0f7fa!important}.cyan-text.text-lighten-5{color:#e0f7fa!important}.cyan.lighten-4{background-color:#b2ebf2!important}.cyan-text.text-lighten-4{color:#b2ebf2!important}.cyan.lighten-3{background-color:#80deea!important}.cyan-text.text-lighten-3{color:#80deea!important}.cyan.lighten-2{background-color:#4dd0e1!important}.cyan-text.text-lighten-2{color:#4dd0e1!important}.cyan.lighten-1{background-color:#26c6da!important}.cyan-text.text-lighten-1{color:#26c6da!important}.cyan.darken-1{background-color:#00acc1!important}.cyan-text.text-darken-1{color:#00acc1!important}.cyan.darken-2{background-color:#0097a7!important}.cyan-text.text-darken-2{color:#0097a7!important}.cyan.darken-3{background-color:#00838f!important}.cyan-text.text-darken-3{color:#00838f!important}.cyan.darken-4{background-color:#006064!important}.cyan-text.text-darken-4{color:#006064!important}.cyan.accent-1{background-color:#84ffff!important}.cyan-text.text-accent-1{color:#84ffff!important}.cyan.accent-2{background-color:#18ffff!important}.cyan-text.text-accent-2{color:#18ffff!important}.cyan.accent-3{background-color:#00e5ff!important}.cyan-text.text-accent-3{color:#00e5ff!important}.cyan.accent-4{background-color:#00b8d4!important}.cyan-text.text-accent-4{color:#00b8d4!important}.teal{background-color:#009688!important}.teal-text{color:#009688!important}.teal.lighten-5{background-color:#e0f2f1!important}.teal-text.text-lighten-5{color:#e0f2f1!important}.teal.lighten-4{background-color:#b2dfdb!important}.teal-text.text-lighten-4{color:#b2dfdb!important}.teal.lighten-3{background-color:#80cbc4!important}.teal-text.text-lighten-3{color:#80cbc4!important}.teal.lighten-2{background-color:#4db6ac!important}.teal-text.text-lighten-2{color:#4db6ac!important}.teal.lighten-1{background-color:#26a69a!important}.teal-text.text-lighten-1{color:#26a69a!important}.teal.darken-1{background-color:#00897b!important}.teal-text.text-darken-1{color:#00897b!important}.teal.darken-2{background-color:#00796b!important}.teal-text.text-darken-2{color:#00796b!important}.teal.darken-3{background-color:#00695c!important}.teal-text.text-darken-3{color:#00695c!important}.teal.darken-4{background-color:#004d40!important}.teal-text.text-darken-4{color:#004d40!important}.teal.accent-1{background-color:#a7ffeb!important}.teal-text.text-accent-1{color:#a7ffeb!important}.teal.accent-2{background-color:#64ffda!important}.teal-text.text-accent-2{color:#64ffda!important}.teal.accent-3{background-color:#1de9b6!important}.teal-text.text-accent-3{color:#1de9b6!important}.teal.accent-4{background-color:#00bfa5!important}.teal-text.text-accent-4{color:#00bfa5!important}.green{background-color:#4caf50!important}.green-text{color:#4caf50!important}.green.lighten-5{background-color:#e8f5e9!important}.green-text.text-lighten-5{color:#e8f5e9!important}.green.lighten-4{background-color:#c8e6c9!important}.green-text.text-lighten-4{color:#c8e6c9!important}.green.lighten-3{background-color:#a5d6a7!important}.green-text.text-lighten-3{color:#a5d6a7!important}.green.lighten-2{background-color:#81c784!important}.green-text.text-lighten-2{color:#81c784!important}.green.lighten-1{background-color:#66bb6a!important}.green-text.text-lighten-1{color:#66bb6a!important}.green.darken-1{background-color:#43a047!important}.green-text.text-darken-1{color:#43a047!important}.green.darken-2{background-color:#388e3c!important}.green-text.text-darken-2{color:#388e3c!important}.green.darken-3{background-color:#2e7d32!important}.green-text.text-darken-3{color:#2e7d32!important}.green.darken-4{background-color:#1b5e20!important}.green-text.text-darken-4{color:#1b5e20!important}.green.accent-1{background-color:#b9f6ca!important}.green-text.text-accent-1{color:#b9f6ca!important}.green.accent-2{background-color:#69f0ae!important}.green-text.text-accent-2{color:#69f0ae!important}.green.accent-3{background-color:#00e676!important}.green-text.text-accent-3{color:#00e676!important}.green.accent-4{background-color:#00c853!important}.green-text.text-accent-4{color:#00c853!important}.light-green{background-color:#8bc34a!important}.light-green-text{color:#8bc34a!important}.light-green.lighten-5{background-color:#f1f8e9!important}.light-green-text.text-lighten-5{color:#f1f8e9!important}.light-green.lighten-4{background-color:#dcedc8!important}.light-green-text.text-lighten-4{color:#dcedc8!important}.light-green.lighten-3{background-color:#c5e1a5!important}.light-green-text.text-lighten-3{color:#c5e1a5!important}.light-green.lighten-2{background-color:#aed581!important}.light-green-text.text-lighten-2{color:#aed581!important}.light-green.lighten-1{background-color:#9ccc65!important}.light-green-text.text-lighten-1{color:#9ccc65!important}.light-green.darken-1{background-color:#7cb342!important}.light-green-text.text-darken-1{color:#7cb342!important}.light-green.darken-2{background-color:#689f38!important}.light-green-text.text-darken-2{color:#689f38!important}.light-green.darken-3{background-color:#558b2f!important}.light-green-text.text-darken-3{color:#558b2f!important}.light-green.darken-4{background-color:#33691e!important}.light-green-text.text-darken-4{color:#33691e!important}.light-green.accent-1{background-color:#ccff90!important}.light-green-text.text-accent-1{color:#ccff90!important}.light-green.accent-2{background-color:#b2ff59!important}.light-green-text.text-accent-2{color:#b2ff59!important}.light-green.accent-3{background-color:#76ff03!important}.light-green-text.text-accent-3{color:#76ff03!important}.light-green.accent-4{background-color:#64dd17!important}.light-green-text.text-accent-4{color:#64dd17!important}.lime{background-color:#cddc39!important}.lime-text{color:#cddc39!important}.lime.lighten-5{background-color:#f9fbe7!important}.lime-text.text-lighten-5{color:#f9fbe7!important}.lime.lighten-4{background-color:#f0f4c3!important}.lime-text.text-lighten-4{color:#f0f4c3!important}.lime.lighten-3{background-color:#e6ee9c!important}.lime-text.text-lighten-3{color:#e6ee9c!important}.lime.lighten-2{background-color:#dce775!important}.lime-text.text-lighten-2{color:#dce775!important}.lime.lighten-1{background-color:#d4e157!important}.lime-text.text-lighten-1{color:#d4e157!important}.lime.darken-1{background-color:#c0ca33!important}.lime-text.text-darken-1{color:#c0ca33!important}.lime.darken-2{background-color:#afb42b!important}.lime-text.text-darken-2{color:#afb42b!important}.lime.darken-3{background-color:#9e9d24!important}.lime-text.text-darken-3{color:#9e9d24!important}.lime.darken-4{background-color:#827717!important}.lime-text.text-darken-4{color:#827717!important}.lime.accent-1{background-color:#f4ff81!important}.lime-text.text-accent-1{color:#f4ff81!important}.lime.accent-2{background-color:#eeff41!important}.lime-text.text-accent-2{color:#eeff41!important}.lime.accent-3{background-color:#c6ff00!important}.lime-text.text-accent-3{color:#c6ff00!important}.lime.accent-4{background-color:#aeea00!important}.lime-text.text-accent-4{color:#aeea00!important}.yellow{background-color:#ffeb3b!important}.yellow-text{color:#ffeb3b!important}.yellow.lighten-5{background-color:#fffde7!important}.yellow-text.text-lighten-5{color:#fffde7!important}.yellow.lighten-4{background-color:#fff9c4!important}.yellow-text.text-lighten-4{color:#fff9c4!important}.yellow.lighten-3{background-color:#fff59d!important}.yellow-text.text-lighten-3{color:#fff59d!important}.yellow.lighten-2{background-color:#fff176!important}.yellow-text.text-lighten-2{color:#fff176!important}.yellow.lighten-1{background-color:#ffee58!important}.yellow-text.text-lighten-1{color:#ffee58!important}.yellow.darken-1{background-color:#fdd835!important}.yellow-text.text-darken-1{color:#fdd835!important}.yellow.darken-2{background-color:#fbc02d!important}.yellow-text.text-darken-2{color:#fbc02d!important}.yellow.darken-3{background-color:#f9a825!important}.yellow-text.text-darken-3{color:#f9a825!important}.yellow.darken-4{background-color:#f57f17!important}.yellow-text.text-darken-4{color:#f57f17!important}.yellow.accent-1{background-color:#ffff8d!important}.yellow-text.text-accent-1{color:#ffff8d!important}.yellow.accent-2{background-color:#ff0!important}.yellow-text.text-accent-2{color:#ff0!important}.yellow.accent-3{background-color:#ffea00!important}.yellow-text.text-accent-3{color:#ffea00!important}.yellow.accent-4{background-color:#ffd600!important}.yellow-text.text-accent-4{color:#ffd600!important}.amber{background-color:#ffc107!important}.amber-text{color:#ffc107!important}.amber.lighten-5{background-color:#fff8e1!important}.amber-text.text-lighten-5{color:#fff8e1!important}.amber.lighten-4{background-color:#ffecb3!important}.amber-text.text-lighten-4{color:#ffecb3!important}.amber.lighten-3{background-color:#ffe082!important}.amber-text.text-lighten-3{color:#ffe082!important}.amber.lighten-2{background-color:#ffd54f!important}.amber-text.text-lighten-2{color:#ffd54f!important}.amber.lighten-1{background-color:#ffca28!important}.amber-text.text-lighten-1{color:#ffca28!important}.amber.darken-1{background-color:#ffb300!important}.amber-text.text-darken-1{color:#ffb300!important}.amber.darken-2{background-color:#ffa000!important}.amber-text.text-darken-2{color:#ffa000!important}.amber.darken-3{background-color:#ff8f00!important}.amber-text.text-darken-3{color:#ff8f00!important}.amber.darken-4{background-color:#ff6f00!important}.amber-text.text-darken-4{color:#ff6f00!important}.amber.accent-1{background-color:#ffe57f!important}.amber-text.text-accent-1{color:#ffe57f!important}.amber.accent-2{background-color:#ffd740!important}.amber-text.text-accent-2{color:#ffd740!important}.amber.accent-3{background-color:#ffc400!important}.amber-text.text-accent-3{color:#ffc400!important}.amber.accent-4{background-color:#ffab00!important}.amber-text.text-accent-4{color:#ffab00!important}.orange{background-color:#ff9800!important}.orange-text{color:#ff9800!important}.orange.lighten-5{background-color:#fff3e0!important}.orange-text.text-lighten-5{color:#fff3e0!important}.orange.lighten-4{background-color:#ffe0b2!important}.orange-text.text-lighten-4{color:#ffe0b2!important}.orange.lighten-3{background-color:#ffcc80!important}.orange-text.text-lighten-3{color:#ffcc80!important}.orange.lighten-2{background-color:#ffb74d!important}.orange-text.text-lighten-2{color:#ffb74d!important}.orange.lighten-1{background-color:#ffa726!important}.orange-text.text-lighten-1{color:#ffa726!important}.orange.darken-1{background-color:#fb8c00!important}.orange-text.text-darken-1{color:#fb8c00!important}.orange.darken-2{background-color:#f57c00!important}.orange-text.text-darken-2{color:#f57c00!important}.orange.darken-3{background-color:#ef6c00!important}.orange-text.text-darken-3{color:#ef6c00!important}.orange.darken-4{background-color:#e65100!important}.orange-text.text-darken-4{color:#e65100!important}.orange.accent-1{background-color:#ffd180!important}.orange-text.text-accent-1{color:#ffd180!important}.orange.accent-2{background-color:#ffab40!important}.orange-text.text-accent-2{color:#ffab40!important}.orange.accent-3{background-color:#ff9100!important}.orange-text.text-accent-3{color:#ff9100!important}.orange.accent-4{background-color:#ff6d00!important}.orange-text.text-accent-4{color:#ff6d00!important}.deep-orange{background-color:#ff5722!important}.deep-orange-text{color:#ff5722!important}.deep-orange.lighten-5{background-color:#fbe9e7!important}.deep-orange-text.text-lighten-5{color:#fbe9e7!important}.deep-orange.lighten-4{background-color:#ffccbc!important}.deep-orange-text.text-lighten-4{color:#ffccbc!important}.deep-orange.lighten-3{background-color:#ffab91!important}.deep-orange-text.text-lighten-3{color:#ffab91!important}.deep-orange.lighten-2{background-color:#ff8a65!important}.deep-orange-text.text-lighten-2{color:#ff8a65!important}.deep-orange.lighten-1{background-color:#ff7043!important}.deep-orange-text.text-lighten-1{color:#ff7043!important}.deep-orange.darken-1{background-color:#f4511e!important}.deep-orange-text.text-darken-1{color:#f4511e!important}.deep-orange.darken-2{background-color:#e64a19!important}.deep-orange-text.text-darken-2{color:#e64a19!important}.deep-orange.darken-3{background-color:#d84315!important}.deep-orange-text.text-darken-3{color:#d84315!important}.deep-orange.darken-4{background-color:#bf360c!important}.deep-orange-text.text-darken-4{color:#bf360c!important}.deep-orange.accent-1{background-color:#ff9e80!important}.deep-orange-text.text-accent-1{color:#ff9e80!important}.deep-orange.accent-2{background-color:#ff6e40!important}.deep-orange-text.text-accent-2{color:#ff6e40!important}.deep-orange.accent-3{background-color:#ff3d00!important}.deep-orange-text.text-accent-3{color:#ff3d00!important}.deep-orange.accent-4{background-color:#dd2c00!important}.deep-orange-text.text-accent-4{color:#dd2c00!important}.brown{background-color:#795548!important}.brown-text{color:#795548!important}.brown.lighten-5{background-color:#efebe9!important}.brown-text.text-lighten-5{color:#efebe9!important}.brown.lighten-4{background-color:#d7ccc8!important}.brown-text.text-lighten-4{color:#d7ccc8!important}.brown.lighten-3{background-color:#bcaaa4!important}.brown-text.text-lighten-3{color:#bcaaa4!important}.brown.lighten-2{background-color:#a1887f!important}.brown-text.text-lighten-2{color:#a1887f!important}.brown.lighten-1{background-color:#8d6e63!important}.brown-text.text-lighten-1{color:#8d6e63!important}.brown.darken-1{background-color:#6d4c41!important}.brown-text.text-darken-1{color:#6d4c41!important}.brown.darken-2{background-color:#5d4037!important}.brown-text.text-darken-2{color:#5d4037!important}.brown.darken-3{background-color:#4e342e!important}.brown-text.text-darken-3{color:#4e342e!important}.brown.darken-4{background-color:#3e2723!important}.brown-text.text-darken-4{color:#3e2723!important}.blue-grey{background-color:#607d8b!important}.blue-grey-text{color:#607d8b!important}.blue-grey.lighten-5{background-color:#eceff1!important}.blue-grey-text.text-lighten-5{color:#eceff1!important}.blue-grey.lighten-4{background-color:#cfd8dc!important}.blue-grey-text.text-lighten-4{color:#cfd8dc!important}.blue-grey.lighten-3{background-color:#b0bec5!important}.blue-grey-text.text-lighten-3{color:#b0bec5!important}.blue-grey.lighten-2{background-color:#90a4ae!important}.blue-grey-text.text-lighten-2{color:#90a4ae!important}.blue-grey.lighten-1{background-color:#78909c!important}.blue-grey-text.text-lighten-1{color:#78909c!important}.blue-grey.darken-1{background-color:#546e7a!important}.blue-grey-text.text-darken-1{color:#546e7a!important}.blue-grey.darken-2{background-color:#455a64!important}.blue-grey-text.text-darken-2{color:#455a64!important}.blue-grey.darken-3{background-color:#37474f!important}.blue-grey-text.text-darken-3{color:#37474f!important}.blue-grey.darken-4{background-color:#263238!important}.blue-grey-text.text-darken-4{color:#263238!important}.grey{background-color:#9e9e9e!important}.grey-text{color:#9e9e9e!important}.grey.lighten-5{background-color:#fafafa!important}.grey-text.text-lighten-5{color:#fafafa!important}.grey.lighten-4{background-color:#f5f5f5!important}.grey-text.text-lighten-4{color:#f5f5f5!important}.grey.lighten-3{background-color:#eee!important}.grey-text.text-lighten-3{color:#eee!important}.grey.lighten-2{background-color:#e0e0e0!important}.grey-text.text-lighten-2{color:#e0e0e0!important}.grey.lighten-1{background-color:#bdbdbd!important}.grey-text.text-lighten-1{color:#bdbdbd!important}.grey.darken-1{background-color:#757575!important}.grey-text.text-darken-1{color:#757575!important}.grey.darken-2{background-color:#616161!important}.grey-text.text-darken-2{color:#616161!important}.grey.darken-3{background-color:#424242!important}.grey-text.text-darken-3{color:#424242!important}.grey.darken-4{background-color:#212121!important}.grey-text.text-darken-4{color:#212121!important}.black{background-color:#000!important}.black-text{color:#000!important}.white{background-color:#fff!important}.white-text{color:#fff!important}.transparent{background-color:transparent!important}.transparent-text{color:transparent!important}
-
-/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:flex;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none!important}.btn,.btn-floating,.btn-large,.btn-small,.card,.card-panel,.collapsible,.dropdown-content,.sidenav,.toast,.z-depth-1,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.12),0 1px 5px 0 rgba(0,0,0,.2)}.btn-floating:hover,.btn-large:hover,.btn-small:hover,.btn:hover,.z-depth-1-half{box-shadow:0 3px 3px 0 rgba(0,0,0,.14),0 1px 7px 0 rgba(0,0,0,.12),0 3px 1px -1px rgba(0,0,0,.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.3)}.z-depth-3{box-shadow:0 8px 17px 2px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.z-depth-4{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -7px rgba(0,0,0,.2)}.modal,.z-depth-5{box-shadow:0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12),0 11px 15px -7px rgba(0,0,0,.2)}.hoverable{transition:box-shadow .25s}.hoverable:hover{box-shadow:0 8px 17px 0 rgba(0,0,0,.2),0 6px 20px 0 rgba(0,0,0,.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width:992px){.pagination{width:100%}.pagination li.next,.pagination li.prev{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:hsla(0,0%,100%,.7)}.breadcrumb [class*=mdi-],.breadcrumb [class^=mdi-],.breadcrumb i,.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:"\E5CC";color:hsla(0,0%,100%,.7);vertical-align:top;display:inline-block;font-family:Material Icons;font-weight:400;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-bottom,.pin-top{position:relative}.pinned{position:fixed!important}.fade-in,ul.staggered-list li{opacity:0}.fade-in{-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width:600px){.hide-on-small-and-down,.hide-on-small-only{display:none!important}}@media only screen and (max-width:992px){.hide-on-med-and-down{display:none!important}}@media only screen and (min-width:601px){.hide-on-med-and-up{display:none!important}}@media only screen and (min-width:600px) and (max-width:992px){.hide-on-med-only{display:none!important}}@media only screen and (min-width:993px){.hide-on-large-only{display:none!important}}@media only screen and (min-width:1201px){.hide-on-extra-large-only{display:none!important}}@media only screen and (min-width:1201px){.show-on-extra-large{display:block!important}}@media only screen and (min-width:993px){.show-on-large{display:block!important}}@media only screen and (min-width:600px) and (max-width:992px){.show-on-medium{display:block!important}}@media only screen and (max-width:600px){.show-on-small{display:block!important}}@media only screen and (min-width:601px){.show-on-medium-and-up{display:block!important}}@media only screen and (max-width:992px){.show-on-medium-and-down{display:block!important}}@media only screen and (max-width:600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:flex;align-items:center;justify-content:space-between;padding:10px 0;color:hsla(0,0%,100%,.8);background-color:rgba(51,51,51,.08)}table,td,th{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:hsla(0,0%,94.9%,.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:hsla(0,0%,94.9%,.5)}table.centered tbody tr td,table.centered thead tr th{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width:992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:"\00a0"}table.responsive-table td,table.responsive-table th{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th:before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,.12)}}.collection{margin:.5rem 0 1rem;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container embed,.video-container iframe,.video-container object{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;transition:width .3s linear}.progress .determinate,.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{-webkit-animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite;animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}.progress .indeterminate:after,.progress .indeterminate:before{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right}.progress .indeterminate:after{-webkit-animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}to{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}to{left:107%;right:-8%}}.hide{display:none!important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left!important}.right{float:right!important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0!important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]:after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:13px}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:"liga";font-feature-settings:"liga"}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width:601px){.container{width:85%}}@media only screen and (min-width:993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*=pull-],.row .col[class*=push-]{position:relative}.row .col.s1{width:8.33333%}.row .col.s1,.row .col.s2{margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.66667%}.row .col.s3{width:25%}.row .col.s3,.row .col.s4{margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.33333%}.row .col.s5{width:41.66667%}.row .col.s5,.row .col.s6{margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%}.row .col.s7{width:58.33333%}.row .col.s7,.row .col.s8{margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.66667%}.row .col.s9{width:75%}.row .col.s9,.row .col.s10{margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.33333%}.row .col.s11{width:91.66667%}.row .col.s11,.row .col.s12{margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%}.row .col.offset-s1{margin-left:8.33333%}.row .col.pull-s1{right:8.33333%}.row .col.push-s1{left:8.33333%}.row .col.offset-s2{margin-left:16.66667%}.row .col.pull-s2{right:16.66667%}.row .col.push-s2{left:16.66667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.33333%}.row .col.pull-s4{right:33.33333%}.row .col.push-s4{left:33.33333%}.row .col.offset-s5{margin-left:41.66667%}.row .col.pull-s5{right:41.66667%}.row .col.push-s5{left:41.66667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.33333%}.row .col.pull-s7{right:58.33333%}.row .col.push-s7{left:58.33333%}.row .col.offset-s8{margin-left:66.66667%}.row .col.pull-s8{right:66.66667%}.row .col.push-s8{left:66.66667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.33333%}.row .col.pull-s10{right:83.33333%}.row .col.push-s10{left:83.33333%}.row .col.offset-s11{margin-left:91.66667%}.row .col.pull-s11{right:91.66667%}.row .col.push-s11{left:91.66667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width:601px){.row .col.m1{width:8.33333%}.row .col.m1,.row .col.m2{margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.66667%}.row .col.m3{width:25%}.row .col.m3,.row .col.m4{margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.33333%}.row .col.m5{width:41.66667%}.row .col.m5,.row .col.m6{margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%}.row .col.m7{width:58.33333%}.row .col.m7,.row .col.m8{margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.66667%}.row .col.m9{width:75%}.row .col.m9,.row .col.m10{margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.33333%}.row .col.m11{width:91.66667%}.row .col.m11,.row .col.m12{margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%}.row .col.offset-m1{margin-left:8.33333%}.row .col.pull-m1{right:8.33333%}.row .col.push-m1{left:8.33333%}.row .col.offset-m2{margin-left:16.66667%}.row .col.pull-m2{right:16.66667%}.row .col.push-m2{left:16.66667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.33333%}.row .col.pull-m4{right:33.33333%}.row .col.push-m4{left:33.33333%}.row .col.offset-m5{margin-left:41.66667%}.row .col.pull-m5{right:41.66667%}.row .col.push-m5{left:41.66667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.33333%}.row .col.pull-m7{right:58.33333%}.row .col.push-m7{left:58.33333%}.row .col.offset-m8{margin-left:66.66667%}.row .col.pull-m8{right:66.66667%}.row .col.push-m8{left:66.66667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.33333%}.row .col.pull-m10{right:83.33333%}.row .col.push-m10{left:83.33333%}.row .col.offset-m11{margin-left:91.66667%}.row .col.pull-m11{right:91.66667%}.row .col.push-m11{left:91.66667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width:993px){.row .col.l1{width:8.33333%}.row .col.l1,.row .col.l2{margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.66667%}.row .col.l3{width:25%}.row .col.l3,.row .col.l4{margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.33333%}.row .col.l5{width:41.66667%}.row .col.l5,.row .col.l6{margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%}.row .col.l7{width:58.33333%}.row .col.l7,.row .col.l8{margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.66667%}.row .col.l9{width:75%}.row .col.l9,.row .col.l10{margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.33333%}.row .col.l11{width:91.66667%}.row .col.l11,.row .col.l12{margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%}.row .col.offset-l1{margin-left:8.33333%}.row .col.pull-l1{right:8.33333%}.row .col.push-l1{left:8.33333%}.row .col.offset-l2{margin-left:16.66667%}.row .col.pull-l2{right:16.66667%}.row .col.push-l2{left:16.66667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.33333%}.row .col.pull-l4{right:33.33333%}.row .col.push-l4{left:33.33333%}.row .col.offset-l5{margin-left:41.66667%}.row .col.pull-l5{right:41.66667%}.row .col.push-l5{left:41.66667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.33333%}.row .col.pull-l7{right:58.33333%}.row .col.push-l7{left:58.33333%}.row .col.offset-l8{margin-left:66.66667%}.row .col.pull-l8{right:66.66667%}.row .col.push-l8{left:66.66667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.33333%}.row .col.pull-l10{right:83.33333%}.row .col.push-l10{left:83.33333%}.row .col.offset-l11{margin-left:91.66667%}.row .col.pull-l11{right:91.66667%}.row .col.push-l11{left:91.66667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width:1201px){.row .col.xl1{width:8.33333%}.row .col.xl1,.row .col.xl2{margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.66667%}.row .col.xl3{width:25%}.row .col.xl3,.row .col.xl4{margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.33333%}.row .col.xl5{width:41.66667%}.row .col.xl5,.row .col.xl6{margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%}.row .col.xl7{width:58.33333%}.row .col.xl7,.row .col.xl8{margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.66667%}.row .col.xl9{width:75%}.row .col.xl9,.row .col.xl10{margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.33333%}.row .col.xl11{width:91.66667%}.row .col.xl11,.row .col.xl12{margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%}.row .col.offset-xl1{margin-left:8.33333%}.row .col.pull-xl1{right:8.33333%}.row .col.push-xl1{left:8.33333%}.row .col.offset-xl2{margin-left:16.66667%}.row .col.pull-xl2{right:16.66667%}.row .col.push-xl2{left:16.66667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.33333%}.row .col.pull-xl4{right:33.33333%}.row .col.push-xl4{left:33.33333%}.row .col.offset-xl5{margin-left:41.66667%}.row .col.pull-xl5{right:41.66667%}.row .col.push-xl5{left:41.66667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.33333%}.row .col.pull-xl7{right:58.33333%}.row .col.push-xl7{left:58.33333%}.row .col.offset-xl8{margin-left:66.66667%}.row .col.pull-xl8{right:66.66667%}.row .col.push-xl8{left:66.66667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.33333%}.row .col.pull-xl10{right:83.33333%}.row .col.push-xl10{left:83.33333%}.row .col.offset-xl11{margin-left:91.66667%}.row .col.pull-xl11{right:91.66667%}.row .col.push-xl11{left:91.66667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav [class*=mdi-],nav [class^=mdi-],nav i,nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width:993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width:992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:.5rem}nav .brand-logo.right{right:.5rem;left:auto}}nav .brand-logo.right{right:.5rem;padding:0}nav .brand-logo [class*=mdi-],nav .brand-logo [class^=mdi-],nav .brand-logo i,nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-flat,nav ul a.btn-floating,nav ul a.btn-large,nav ul a.btn-small{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=date]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=text]:valid,nav .input-field input[type=url]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:hsla(0,0%,100%,.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width:601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-weight:400;color:rgba(0,0,0,.87)}@media only screen and (min-width:0){html{font-size:14px}}@media only screen and (min-width:992px){html{font-size:14.5px}}@media only screen and (min-width:1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;margin:2.8rem 0 1.68rem}h1,h2{line-height:110%}h2{font-size:3.56rem;margin:2.37333rem 0 1.424rem}h3{font-size:2.92rem;margin:1.94667rem 0 1.168rem}h3,h4{line-height:110%}h4{font-size:2.28rem;margin:1.52rem 0 .912rem}h5{font-size:1.64rem;margin:1.09333rem 0 .656rem}h5,h6{line-height:110%}h6{font-size:1.15rem;margin:.76667rem 0 .46rem}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width:360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width:390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width:420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width:450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width:480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width:510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width:540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width:570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width:600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width:630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width:660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width:690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width:720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width:750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width:780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width:810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width:840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width:870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width:900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width:930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width:960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width:360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63)!important;transition:transform .3s cubic-bezier(.53,.01,.36,1.63),-webkit-transform .3s cubic-bezier(.53,.01,.36,1.63)!important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .2s!important;transition:transform .2s!important;transition:transform .2s,-webkit-transform .2s!important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{padding:24px}.card,.card-panel{transition:box-shadow .25s;margin:.5rem 0 1rem;border-radius:2px;background-color:#eceff1}.card{position:relative}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.large,.card.medium,.card.small{position:relative}.card.large .card-image,.card.medium .card-image,.card.small .card-image{max-height:60%;overflow:hidden}.card.large .card-image+.card-content,.card.medium .card-image+.card-content,.card.small .card-image+.card-content{max-height:40%}.card.large .card-content,.card.medium .card-content,.card.small .card-content{max-height:100%;overflow:hidden}.card.large .card-action,.card.medium .card-action,.card.small .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:flex}.card.horizontal.large .card-image,.card.horizontal.medium .card-image,.card.horizontal.small .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.large .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.small .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:flex;flex-direction:column;flex:1;position:relative}.card.horizontal .card-stacked .card-content{flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#eceff1;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid hsla(0,0%,62.7%,.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#039be5;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#51c5fd}.card .card-reveal{padding:24px;position:absolute;background-color:#eceff1;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width:600px){#toast-container{min-width:100%;bottom:0}}@media only screen and (min-width:601px) and (max-width:992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width:993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:flex;align-items:center;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width:600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover,.tabs.tabs-transparent .tab a{color:hsla(0,0%,100%,.7)}.tabs.tabs-transparent .tab a.active,.tabs.tabs-transparent .tab a:hover{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:flex}.tabs.tabs-fixed-width .tab{flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease,background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,.2);outline:none}.tabs .tab a.active,.tabs .tab a:hover{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left,right}@media only screen and (max-width:992px){.tabs{display:flex}.tabs .tab{flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none}.backdrop,.material-tooltip{opacity:0;position:absolute;visibility:hidden;background-color:#323232}.backdrop{height:7px;width:14px;border-radius:0 0 50% 50%;z-index:-1;-webkit-transform-origin:50% 0;transform-origin:50% 0}.btn,.btn-flat,.btn-large,.btn-small{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn-flat.disabled,.btn-flat:disabled,.btn-flat[disabled],.btn-floating.disabled,.btn-floating:disabled,.btn-floating[disabled],.btn-large.disabled,.btn-large:disabled,.btn-large[disabled],.btn-small.disabled,.btn-small:disabled,.btn-small[disabled],.btn.disabled,.btn:disabled,.btn[disabled],.disabled.btn-large,.disabled.btn-small{pointer-events:none;background-color:#dfdfdf!important;box-shadow:none;color:#9f9f9f!important;cursor:default}.btn-flat.disabled:hover,.btn-flat:disabled:hover,.btn-flat[disabled]:hover,.btn-floating.disabled:hover,.btn-floating:disabled:hover,.btn-floating[disabled]:hover,.btn-large.disabled:hover,.btn-large:disabled:hover,.btn-large[disabled]:hover,.btn-small.disabled:hover,.btn-small:disabled:hover,.btn-small[disabled]:hover,.btn.disabled:hover,.btn:disabled:hover,.btn[disabled]:hover,.disabled.btn-large:hover,.disabled.btn-small:hover{background-color:#dfdfdf!important;color:#9f9f9f!important}.btn,.btn-flat,.btn-floating,.btn-large,.btn-small{font-size:14px;outline:0}.btn-flat i,.btn-floating i,.btn-large i,.btn-small i,.btn i{font-size:1.3rem;line-height:inherit}.btn-floating:focus,.btn-large:focus,.btn-small:focus,.btn:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;transition:background-color .2s ease-out;cursor:pointer}.btn-large:hover,.btn-small:hover,.btn:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;border-radius:50%;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating,.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:flex;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{background-color:transparent;color:#343434;cursor:pointer;transition:background-color .2s}.btn-flat,.btn-flat:focus,.btn-flat:hover{box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,.1)}.btn-flat.btn-flat[disabled],.btn-flat.disabled{background-color:transparent!important;color:#b3b3b3!important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li.active,.dropdown-content li:hover{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type=checkbox]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}
-
-/*!
- * Waves v0.6.0
- * http://fian.my.id/Waves
- *
- * Copyright 2014 Alfiana E. Sibuea and other contributors
- * Released under the MIT license
- * https://github.com/fians/Waves/blob/master/LICENSE
- */.dropdown-trigger,.waves-effect{cursor:pointer}.waves-effect{position:relative;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.2);transition:all .7s ease-out;transition-property:opacity,-webkit-transform;transition-property:transform,opacity;transition-property:transform,opacity,-webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:hsla(0,0%,100%,.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,.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{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle,#fff 100%,#000 0)}.waves-input-wrapper{border-radius:.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-flat,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small{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,.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}.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,.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,.16),0 2px 10px 0 rgba(0,0,0,.12);margin:0 24px;transition:margin .35s cubic-bezier(.25,.46,.45,.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,.18),0 4px 15px 0 rgba(0,0,0,.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(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;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,.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:.8rem;-webkit-transform:translateY(-140%);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{top:0;right:0;background-color:#292929;will-change:opacity}#materialbox-overlay,.materialbox-caption{position:fixed;bottom:0;left:0;z-index:1000}.materialbox-caption{display:none;color:#fff;line-height:50px;width:100%;text-align:center;padding:0 15%;height:50px;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=date]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=email]:not(.browser-default),input[type=number]:not(.browser-default),input[type=password]:not(.browser-default),input[type=search]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=text]:not(.browser-default),input[type=time]:not(.browser-default),input[type=url]: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;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=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly=readonly],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly=readonly],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly=readonly],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly=readonly],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly=readonly],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly=readonly],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly=readonly],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly=readonly],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly=readonly],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly=readonly],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly=readonly],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly=readonly]{color:rgba(0,0,0,.42);border-bottom:1px dotted rgba(0,0,0,.42)}input:not([type]):disabled+label,input:not([type])[readonly=readonly]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]: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=datetime]:not(.browser-default):disabled+label,input[type=datetime]: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=number]:not(.browser-default):disabled+label,input[type=number]: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=search]:not(.browser-default):disabled+label,input[type=search]: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=text]:not(.browser-default):disabled+label,input[type=text]: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=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly=readonly]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly=readonly]+label{color:rgba(0,0,0,.42)}input:not([type]):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=url]: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=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#4caf50}input:not([type]):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#f44336}input:not([type]).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=url]: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.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]: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.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]: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.invalid~.helper-text[data-error],.select-wrapper.valid .helper-text[data-success],input:not([type]).invalid~.helper-text[data-error],input:not([type]).valid~.helper-text[data-success],input:not([type]):focus.invalid~.helper-text[data-error],input:not([type]):focus.valid~.helper-text[data-success],input[type=date]:not(.browser-default).invalid~.helper-text[data-error],input[type=date]:not(.browser-default).valid~.helper-text[data-success],input[type=date]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=date]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid~.helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid~.helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=datetime]:not(.browser-default).invalid~.helper-text[data-error],input[type=datetime]:not(.browser-default).valid~.helper-text[data-success],input[type=datetime]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=datetime]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=email]:not(.browser-default).invalid~.helper-text[data-error],input[type=email]:not(.browser-default).valid~.helper-text[data-success],input[type=email]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=email]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=number]:not(.browser-default).invalid~.helper-text[data-error],input[type=number]:not(.browser-default).valid~.helper-text[data-success],input[type=number]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=number]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=password]:not(.browser-default).invalid~.helper-text[data-error],input[type=password]:not(.browser-default).valid~.helper-text[data-success],input[type=password]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=password]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=search]:not(.browser-default).invalid~.helper-text[data-error],input[type=search]:not(.browser-default).valid~.helper-text[data-success],input[type=search]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=search]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=tel]:not(.browser-default).invalid~.helper-text[data-error],input[type=tel]:not(.browser-default).valid~.helper-text[data-success],input[type=tel]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=tel]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=text]:not(.browser-default).invalid~.helper-text[data-error],input[type=text]:not(.browser-default).valid~.helper-text[data-success],input[type=text]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=text]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=time]:not(.browser-default).invalid~.helper-text[data-error],input[type=time]:not(.browser-default).valid~.helper-text[data-success],input[type=time]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=time]:not(.browser-default):focus.valid~.helper-text[data-success],input[type=url]:not(.browser-default).invalid~.helper-text[data-error],input[type=url]:not(.browser-default).valid~.helper-text[data-success],input[type=url]:not(.browser-default):focus.invalid~.helper-text[data-error],input[type=url]:not(.browser-default):focus.valid~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea:focus.invalid~.helper-text[data-error],textarea.materialize-textarea:focus.valid~.helper-text[data-success]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input:not([type]).valid~.helper-text:after,input:not([type]):focus.valid~.helper-text:after,input[type=date]:not(.browser-default).valid~.helper-text:after,input[type=date]:not(.browser-default):focus.valid~.helper-text:after,input[type=datetime-local]:not(.browser-default).valid~.helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid~.helper-text:after,input[type=datetime]:not(.browser-default).valid~.helper-text:after,input[type=datetime]:not(.browser-default):focus.valid~.helper-text:after,input[type=email]:not(.browser-default).valid~.helper-text:after,input[type=email]:not(.browser-default):focus.valid~.helper-text:after,input[type=number]:not(.browser-default).valid~.helper-text:after,input[type=number]:not(.browser-default):focus.valid~.helper-text:after,input[type=password]:not(.browser-default).valid~.helper-text:after,input[type=password]:not(.browser-default):focus.valid~.helper-text:after,input[type=search]:not(.browser-default).valid~.helper-text:after,input[type=search]:not(.browser-default):focus.valid~.helper-text:after,input[type=tel]:not(.browser-default).valid~.helper-text:after,input[type=tel]:not(.browser-default):focus.valid~.helper-text:after,input[type=text]:not(.browser-default).valid~.helper-text:after,input[type=text]:not(.browser-default):focus.valid~.helper-text:after,input[type=time]:not(.browser-default).valid~.helper-text:after,input[type=time]:not(.browser-default):focus.valid~.helper-text:after,input[type=url]:not(.browser-default).valid~.helper-text:after,input[type=url]:not(.browser-default):focus.valid~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea:focus.valid~.helper-text:after{content:attr(data-success);color:#4caf50}.select-wrapper.invalid~.helper-text:after,input:not([type]).invalid~.helper-text:after,input:not([type]):focus.invalid~.helper-text:after,input[type=date]:not(.browser-default).invalid~.helper-text:after,input[type=date]:not(.browser-default):focus.invalid~.helper-text:after,input[type=datetime-local]:not(.browser-default).invalid~.helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid~.helper-text:after,input[type=datetime]:not(.browser-default).invalid~.helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid~.helper-text:after,input[type=email]:not(.browser-default).invalid~.helper-text:after,input[type=email]:not(.browser-default):focus.invalid~.helper-text:after,input[type=number]:not(.browser-default).invalid~.helper-text:after,input[type=number]:not(.browser-default):focus.invalid~.helper-text:after,input[type=password]:not(.browser-default).invalid~.helper-text:after,input[type=password]:not(.browser-default):focus.invalid~.helper-text:after,input[type=search]:not(.browser-default).invalid~.helper-text:after,input[type=search]:not(.browser-default):focus.invalid~.helper-text:after,input[type=tel]:not(.browser-default).invalid~.helper-text:after,input[type=tel]:not(.browser-default):focus.invalid~.helper-text:after,input[type=text]:not(.browser-default).invalid~.helper-text:after,input[type=text]:not(.browser-default):focus.invalid~.helper-text:after,input[type=time]:not(.browser-default).invalid~.helper-text:after,input[type=time]:not(.browser-default):focus.invalid~.helper-text:after,input[type=url]:not(.browser-default).invalid~.helper-text:after,input[type=url]:not(.browser-default):focus.invalid~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea:focus.invalid~.helper-text:after{content:attr(data-error);color:#f44336}.select-wrapper+label:after,input:not([type])+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=text]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:opacity .2s ease-out,color .2s 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 .select-dropdown,.input-field.inline input{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~.validate~label,.input-field.col .prefix~label{width:calc(100% - 4.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:color .2s ease-out,-webkit-transform .2s ease-out;transition:transform .2s ease-out,color .2s ease-out;transition:transform .2s ease-out,color .2s ease-out,-webkit-transform .2s ease-out;-webkit-transform-origin:0 100%;transform-origin:0 100%;text-align:left;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(.8);transform:translateY(-14px) scale(.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label,.input-field>input[type]:-webkit-autofill:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(.8);transform:translateY(-14px) scale(.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(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~.autocomplete-content,.input-field .prefix~.helper-text,.input-field .prefix~.validate~label,.input-field .prefix~input,.input-field .prefix~label,.input-field .prefix~textarea{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:background-color .3s}.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)~.material-icons,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search]~.material-icons,.input-field input[type=search]~.mdi-navigation-close{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:color .3s}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-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]:checked,[type=radio]:not(:checked){position:absolute;opacity:0;pointer-events:none}[type=radio]:checked+span,[type=radio]:not(:checked)+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type=radio]+span:after,[type=radio]+span:before{content:"";position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type=radio].with-gap:checked+span:after,[type=radio].with-gap:checked+span:before,[type=radio]:checked+span:after,[type=radio]:checked+span:before,[type=radio]:not(:checked)+span:after,[type=radio]:not(:checked)+span:before{border-radius:50%}[type=radio]:not(:checked)+span:after,[type=radio]:not(:checked)+span:before{border:2px solid #5a5a5a}[type=radio]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type=radio]:checked+span:before{border:2px solid transparent}[type=radio].with-gap:checked+span:after,[type=radio].with-gap:checked+span:before,[type=radio]:checked+span:after{border:2px solid #26a69a}[type=radio].with-gap:checked+span:after,[type=radio]:checked+span:after{background-color:#26a69a}[type=radio]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type=radio].with-gap:checked+span:after{-webkit-transform:scale(.5);transform:scale(.5)}[type=radio].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,.1)}[type=radio].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,.42)}[type=radio].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,.42)}[type=radio]:disabled:checked+span:before,[type=radio]:disabled:not(:checked)+span:before{background-color:transparent;border-color:rgba(0,0,0,.42)}[type=radio]:disabled+span{color:rgba(0,0,0,.42)}[type=radio]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,.42)}[type=radio]:disabled:checked+span:after{background-color:rgba(0,0,0,.42);border-color:#949494}[type=checkbox]:checked,[type=checkbox]:not(: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;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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;-webkit-transform:scale(0);transform:scale(0)}[type=checkbox]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,.42)}[type=checkbox].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}[type=checkbox]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-color:transparent #26a69a #26a69a transparent;border-style:solid;border-width:2px;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,.42);border-bottom:2px solid rgba(0,0,0,.42)}[type=checkbox]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border:none;border-right:2px solid #26a69a;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type=checkbox]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(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):after,[type=checkbox].filled-in+span:not(.lever):before{content:"";left:0;position:absolute;transition:border .25s,background-color .25s,width .2s .1s,height .2s .1s,top .2s .1s,left .2s .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;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;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:0;z-index:0}[type=checkbox].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-color:transparent #fff #fff transparent;border-style:solid;border-width:2px;-webkit-transform:rotate(37deg);transform:rotate(37deg);-webkit-transform-origin:100% 100%;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,.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;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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:after,.switch label input[type=checkbox]:checked+.lever:before{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,.38);border-radius:15px;transition:background .3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:after,.switch label .lever:before{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left .3s ease,background .3s ease,box-shadow .1s ease,-webkit-transform .1s ease;transition:left .3s ease,background .3s ease,box-shadow .1s ease,transform .1s ease;transition:left .3s ease,background .3s ease,box-shadow .1s ease,transform .1s ease,-webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,.15)}.switch label .lever:after{background-color:#f1f1f1;box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever:before,input[type=checkbox]:checked:not(:disabled)~.lever:active:before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,.15)}input[type=checkbox]:not(:disabled).tabbed:focus~.lever:before,input[type=checkbox]:not(:disabled)~.lever:active:before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(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:hsla(0,0%,100%,.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.invalid+label,.select-wrapper.valid+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;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}.select-wrapper.disabled+label,select:disabled{color:rgba(0,0,0,.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,.3);background-color:transparent}.select-dropdown.dropdown-content li:hover,body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(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,.7)}.select-dropdown li.optgroup>span{color:rgba(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;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);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;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(38,166,154,.26)}input[type=range]{border:1px solid #fff}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,.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,.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{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;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:calc(100% + 60px);height:100%;padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,.05)}.sidenav li>a{color:rgba(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,.05)}.sidenav li>a.btn,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating,.sidenav li>a.btn-large,.sidenav li>a.btn-small{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-floating,.sidenav li>a.btn-large,.sidenav li>a.btn-small{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>[class^=mdi-],.sidenav li>a>i,.sidenav li>a>i.material-icons,.sidenav li>a li>a>[class*=mdi-]{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,.54)}.sidenav .divider{margin:8px 0 0}.sidenav .subheader{cursor:auto;pointer-events:none;color:rgba(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 .email,.sidenav .user-view .name{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .email,.sidenav .user-view .name{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;-webkit-transform:translateX(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{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);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,.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(1turn)}}@keyframes container-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.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(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,blue-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,red-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both,green-fade-in-out 5332ms cubic-bezier(.4,0,.2,1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-green-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(.4,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(3turn)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}@-webkit-keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@keyframes blue-fade-in-out{0%{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}to{opacity:1}}@-webkit-keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{0%{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{0%{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{opacity:0}}@keyframes green-fade-in-out{0%{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}to{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:3px 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(.4,0,.2,1) infinite both;animation:left-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both;animation:right-spin 1333ms cubic-bezier(.4,0,.2,1) infinite both}@-webkit-keyframes left-spin{0%{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{0%{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{0%{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{0%{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1);animation:container-rotate 1568ms linear infinite,fade-out .4s cubic-bezier(.4,0,.2,1)}@-webkit-keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes fade-out{0%{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:50%}.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;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0 50%;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:hsla(0,0%,100%,.5);transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel .carousel-item:not(.active) .materialboxed,.carousel.scrolling .carousel-item .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{opacity:.95;transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-wrapper.open .tap-target,.tap-target-wrapper.open .tap-target-wave:before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave:after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;transition:opacity .3s,visibility 0s 1s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s 1s;transition:opacity .3s,transform .3s,visibility 0s 1s,-webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,.14),0 10px 50px 0 rgba(0,0,0,.12),0 30px 10px -20px rgba(0,0,0,.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);transition:opacity .3s ease-in-out,-webkit-transform .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out;transition:transform .3s ease-in-out,opacity .3s ease-in-out,-webkit-transform .3s ease-in-out}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave:after,.tap-target-wave:before{content:"";display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#fff}.tap-target-wave:before{-webkit-transform:scale(0);transform:scale(0);transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.tap-target-wave:after{visibility:hidden;transition:opacity .3s,visibility 0s,-webkit-transform .3s;transition:opacity .3s,transform .3s,visibility 0s;transition:opacity .3s,transform .3s,visibility 0s,-webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%,-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,-webkit-transform .3s;transition:opacity .3s,transform .3s;transition:opacity .3s,transform .3s,-webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;animation:pulse-animation 1s cubic-bezier(.24,0,.38,1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}to{opacity:0;-webkit-transform:scale(1.5);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-next,.month-prev{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:hsla(0,0%,100%,.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-disabled,.datepicker-table td.is-outside-current-month{color:rgba(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,.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-done,.datepicker-today{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-footer,.datepicker-table{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:700;text-align:center;color:hsla(0,0%,100%,.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-am-pm div,.timepicker-span-hours,.timepicker-span-minutes{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:25px auto 5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;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,.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,.25)}.timepicker-dial{transition:opacity .35s,-webkit-transform .35s;transition:transform .35s,opacity .35s;transition:transform .35s,opacity .35s,-webkit-transform .35s}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1);transform:scale(1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(.8);transform:scale(.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:.25}.timepicker-canvas-bearing,.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,.87)}.contrast-color-text{color:rgba(0,0,0,.87)}.success-color{background-color:#4caf50}.success-color-text{color:#4caf50}html{background-color:#fff}.pagination li a,a.unmarked{color:rgba(0,0,0,.87)}.progress{background-color:#e0e0e0}.progress>.determinate{background-color:#2196f3}
+.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:flex;align-items:center}.clearfix{clear:both}.z-depth-0{box-shadow:none !important}.z-depth-1,.sidenav,.collapsible,.dropdown-content,.btn-floating,.btn,.btn-small,.btn-large,.toast,.card,.card-panel,nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn-floating:hover,.btn:hover,.btn-small:hover,.btn-large:hover{box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{transition:box-shadow .25s}.hoverable:hover{box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width : 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;transform:translate3d(0, 0, 0);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;transform-origin:0 50%}@media only screen and (max-width : 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width : 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width : 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width : 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width : 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width : 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width : 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width : 600px){.show-on-small{display:block !important}}@media only screen and (min-width : 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width : 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width : 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:flex;align-items:center;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width : 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;transition:.25s;color:#000}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation-delay:1.15s}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width : 601px){.container{width:85%}}@media only screen and (min-width : 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width : 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width : 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width : 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width : 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;transform:translateX(-50%)}@media only screen and (max-width : 992px){nav .brand-logo{left:50%;transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width : 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{transform:scale(0);transition:transform .2s !important}.scale-transition.scale-in{transform:scale(1)}.card-panel{transition:box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#eceff1}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#eceff1;transition:box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:flex;flex-direction:column;flex:1;position:relative}.card.horizontal .card-stacked .card-content{flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#eceff1;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-small):not(.btn-large):not(.btn-large):not(.btn-floating){color:#039be5;margin-right:24px;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-small):not(.btn-large):not(.btn-large):not(.btn-floating):hover{color:#51c5fd}.card .card-reveal{padding:24px;position:absolute;background-color:#eceff1;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width : 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width : 601px) and (max-width : 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width : 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:flex;align-items:center;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width : 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:flex}.tabs.tabs-fixed-width .tab{flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width : 992px){.tabs{display:flex}.tabs .tab{flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;transform-origin:50% 0%;visibility:hidden}.btn,.btn-small,.btn-large,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-small:focus,.btn-large:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-small,.btn-large{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-small:hover,.btn-large:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:flex;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{flex:1;display:inline-block;margin:0;height:100%;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;transform:scale(0)}.btn-flat{box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b3b3 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;transform:none}.dropdown-trigger{cursor:pointer}/*!
+ * Waves v0.6.0
+ * http://fian.my.id/Waves
+ *
+ * Copyright 2014 Alfiana E. Sibuea and other contributors
+ * Released under the MIT license
+ * https://github.com/fians/Waves/blob/master/LICENSE
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#000}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#4CAF50}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #4CAF50;box-shadow:0 1px 0 0 #4CAF50}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #F44336;box-shadow:0 1px 0 0 #F44336}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#4CAF50}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#F44336}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#000;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#616161;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:rgba(0,0,0,0.87)}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#F44336}.caution-color-text{color:#F44336}.info-color{background-color:#fff9c4}.info-color-text{color:#fff9c4}.contrast-color{background-color:rgba(0,0,0,0.87)}.contrast-color-text{color:rgba(0,0,0,0.87)}.success-color{background-color:#4CAF50}.success-color-text{color:#4CAF50}html{background-color:#fff}a.unmarked{color:rgba(0,0,0,0.87)}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:rgba(0,0,0,0.87)}.collection-item{color:rgba(0,0,0,0.87)}.collection-item .secondary-content{color:rgba(0,0,0,0.87)}.collection-item.disabled{color:#616161}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#eee;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#eee;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#FFCDD2;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:rgba(0,0,0,0.87)}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:rgba(0,0,0,0.87)}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.departures .dep-dest .followee-checkin i.material-icons{vertical-align:middle}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:rgba(0,0,0,0.87);position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.RUF,.dep-line.AST{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.STB,.dep-line.M{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}.progress{background-color:#e0e0e0}.progress>.determinate{background-color:#2196F3}
diff --git a/public/static/css/local.css b/public/static/css/local.css
deleted file mode 100644
index 2eb14f0..0000000
--- a/public/static/css/local.css
+++ /dev/null
@@ -1,46 +0,0 @@
-.action-checkin,
-.action-checkout,
-.action-undo,
-.action-cancelled-from,
-.action-cancelled-to,
-.action-share {
- cursor: pointer;
-}
-
-td.cancelled {
- text-decoration: line-through;
-}
-
-.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;
-}
diff --git a/public/static/css/material-icons.css b/public/static/css/material-icons.css
index 07005de..73103dd 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/v32/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
+ src: url(/static/v72/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
- url(/static/v32/fonts/MaterialIcons-Regular.woff2) format('woff2'),
- url(/static/v32/fonts/MaterialIcons-Regular.woff) format('woff'),
- url(/static/v32/fonts/MaterialIcons-Regular.ttf) format('truetype');
+ url(/static/v72/fonts/MaterialIcons-Regular.woff2) format('woff2'),
+ url(/static/v72/fonts/MaterialIcons-Regular.woff) format('woff'),
+ url(/static/v72/fonts/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
diff --git a/public/static/fonts/MaterialIcons-Regular.svg b/public/static/fonts/MaterialIcons-Regular.svg
index a449327..f57725c 100644
--- a/public/static/fonts/MaterialIcons-Regular.svg
+++ b/public/static/fonts/MaterialIcons-Regular.svg
@@ -5,9 +5,9 @@
-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
-Created by FontForge 20151118 at Mon Feb 8 11:58:02 2016
- By shyndman
-Copyright 2015 Google, Inc. All Rights Reserved.
+Copyright 2014-2019 Materialize
+
+SPDX-License-Identifier: Apache-2.0
</metadata>
<defs>
<font id="MaterialIcons-Regular" horiz-adv-x="512" >
diff --git a/public/static/icons/touch-icon-120x120.png b/public/static/icons/touch-icon-120x120.png
new file mode 100644
index 0000000..4193fcf
--- /dev/null
+++ b/public/static/icons/touch-icon-120x120.png
Binary files differ
diff --git a/public/static/icons/touch-icon-128x128.png b/public/static/icons/touch-icon-128x128.png
new file mode 100644
index 0000000..6475a7d
--- /dev/null
+++ b/public/static/icons/touch-icon-128x128.png
Binary files differ
diff --git a/public/static/icons/touch-icon-144x144.png b/public/static/icons/touch-icon-144x144.png
new file mode 100644
index 0000000..41e9bee
--- /dev/null
+++ b/public/static/icons/touch-icon-144x144.png
Binary files differ
diff --git a/public/static/icons/touch-icon-152x152.png b/public/static/icons/touch-icon-152x152.png
new file mode 100644
index 0000000..d3ceb86
--- /dev/null
+++ b/public/static/icons/touch-icon-152x152.png
Binary files differ
diff --git a/public/static/icons/touch-icon-167x167.png b/public/static/icons/touch-icon-167x167.png
new file mode 100644
index 0000000..8eb0337
--- /dev/null
+++ b/public/static/icons/touch-icon-167x167.png
Binary files differ
diff --git a/public/static/icons/touch-icon-16x16.png b/public/static/icons/touch-icon-16x16.png
new file mode 100644
index 0000000..96df578
--- /dev/null
+++ b/public/static/icons/touch-icon-16x16.png
Binary files differ
diff --git a/public/static/icons/touch-icon-180x180.png b/public/static/icons/touch-icon-180x180.png
new file mode 100644
index 0000000..c51197f
--- /dev/null
+++ b/public/static/icons/touch-icon-180x180.png
Binary files differ
diff --git a/public/static/icons/touch-icon-192x192.png b/public/static/icons/touch-icon-192x192.png
new file mode 100644
index 0000000..1263811
--- /dev/null
+++ b/public/static/icons/touch-icon-192x192.png
Binary files differ
diff --git a/public/static/icons/touch-icon-256x256.png b/public/static/icons/touch-icon-256x256.png
new file mode 100644
index 0000000..a0893ba
--- /dev/null
+++ b/public/static/icons/touch-icon-256x256.png
Binary files differ
diff --git a/public/static/icons/touch-icon-32x32.png b/public/static/icons/touch-icon-32x32.png
new file mode 100644
index 0000000..5ac212e
--- /dev/null
+++ b/public/static/icons/touch-icon-32x32.png
Binary files differ
diff --git a/public/static/icons/touch-icon-512x512.png b/public/static/icons/touch-icon-512x512.png
new file mode 100644
index 0000000..654de53
--- /dev/null
+++ b/public/static/icons/touch-icon-512x512.png
Binary files differ
diff --git a/public/static/icons/touch-icon-96x96.png b/public/static/icons/touch-icon-96x96.png
new file mode 100644
index 0000000..bd941dc
--- /dev/null
+++ b/public/static/icons/touch-icon-96x96.png
Binary files differ
diff --git a/public/static/icons/touch-icon.svg b/public/static/icons/touch-icon.svg
new file mode 100644
index 0000000..ae43ad7
--- /dev/null
+++ b/public/static/icons/touch-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path style="fill:none" d="M0 0h180v180H0z"/><path style="fill:#673ab7" d="M0 0h162v172H0z" transform="scale(1.11111 1.04651)"/><path d="M8.427 1.305c-3.062 0-6.123.383-6.123 3.061v7.271a2.682 2.682 0 0 0 2.679 2.679l-1.148 1.148v.383h1.706l1.531-1.531h2.885l1.531 1.531h1.531v-.383l-1.148-1.148a2.681 2.681 0 0 0 2.678-2.679V4.366c0-2.678-2.74-3.061-6.122-3.061Zm-3.444 11.48a1.146 1.146 0 0 1-1.148-1.148c0-.635.512-1.148 1.148-1.148a1.147 1.147 0 1 1 0 2.296Zm2.678-5.357H3.835V4.366h3.826v3.062Zm1.531 0V4.366h3.827v3.062H9.192Zm2.679 5.357a1.146 1.146 0 0 1-1.148-1.148c0-.635.512-1.148 1.148-1.148a1.147 1.147 0 1 1 0 2.296ZM13.209 20.997l-3.591-3.59-1.222 1.214 4.813 4.813 10.332-10.332-1.214-1.214-9.118 9.109Z" style="fill:#fff;fill-rule:nonzero" transform="translate(4.812 8.457) scale(6.59229)"/></svg> \ No newline at end of file
diff --git a/public/static/js/autocomplete.js b/public/static/js/autocomplete.js
deleted file mode 100644
index ffc129c..0000000
--- a/public/static/js/autocomplete.js
+++ /dev/null
@@ -1,8642 +0,0 @@
-document.addEventListener('DOMContentLoaded', function() {
- var elems = document.querySelectorAll('.autocomplete');
- M.Autocomplete.init(elems, {
- minLength: 3,
- limit: 50,
- data: {
- "Aachen Hbf": null,
- "Aachen Schanz": null,
- "Aachen West": null,
- "Aachen West": null,
- "Aachen-Rothe Erde": null,
- "Aachen-Rothe Erde": null,
- "Aalen Hbf": null,
- "Abenden": null,
- "Abensberg": null,
- "Achern": null,
- "Achern Stadt": null,
- "Achim": null,
- "Achkarren": null,
- "Achmer": null,
- "Achterwehr": null,
- "Adelebsen": null,
- "Adelschlag": null,
- "Adelsdorf(Mittelfr)": null,
- "Adelsheim Nord": null,
- "Adelsheim Ost": null,
- "Adorf(Erzgeb)": null,
- "Adorf(Vogtl)": null,
- "Affaltrach": null,
- "Agatharied": null,
- "Agathenburg": null,
- "Aglasterhausen": null,
- "Aha": null,
- "Ahaus": null,
- "Ahlbeck Grenze": null,
- "Ahlbeck Ostseetherme": null,
- "Ahlen(Westf)": null,
- "Ahlhorn": null,
- "Ahlten": null,
- "Ahnatal Casselbreite": null,
- "Ahnatal-Heckershausen": null,
- "Ahnatal-Weimar": null,
- "Ahrbrück": null,
- "Ahrensburg": null,
- "Ahrensburg-Gartenholz": null,
- "Ahrensfelde": null,
- "Ahrensfelde Friedhof": null,
- "Ahrensfelde Nord": null,
- "Ahrweiler": null,
- "Ahrweiler Markt": null,
- "Aich(Niederbay)": null,
- "Aichach": null,
- "Aichstetten": null,
- "Aindorf": null,
- "Ainring": null,
- "Albbruck": null,
- "Albersdorf": null,
- "Albersweiler(Pfalz)": null,
- "Albig": null,
- "Albig": null,
- "Albisheim(Pfrimm)": null,
- "Albrechtshaus": null,
- "Albrechtshof": null,
- "Albshausen": null,
- "Albsheim(Eis)": null,
- "Albstadt-Ebingen": null,
- "Albstadt-Ebingen West": null,
- "Albstadt-Laufen Ort": null,
- "Albstadt-Lautlingen": null,
- "Aldekerk": null,
- "Aldingen(b Spaichingen)": null,
- "Aletshausen": null,
- "Alexisbad": null,
- "Alfeld(Leine)": null,
- "Alfter-Impekoven": null,
- "Alfter-Witterschlick": null,
- "Algermissen": null,
- "Aligse": null,
- "Allendorf(Dillkr)": null,
- "Allensbach": null,
- "Allersberg(Rothsee)": null,
- "Allmendingen": null,
- "Alpen": null,
- "Alpirsbach": null,
- "Alsdorf Poststraße": null,
- "Alsdorf(Westerw)": null,
- "Alsdorf-Annapark": null,
- "Alsdorf-Busch": null,
- "Alsdorf-Kellersberg": null,
- "Alsdorf-Mariadorf": null,
- "Alsenz": null,
- "Alsfeld(Oberhess)": null,
- "Alsheim": null,
- "Alt Hüttendorf": null,
- "Alt Rosenthal": null,
- "Altbach": null,
- "Altdorf West": null,
- "Altdorf(Niederbay)": null,
- "Altdorf(b Nürnberg)": null,
- "Altdöbern": null,
- "Alte Veste": null,
- "Altefähr": null,
- "Altena(Westf)": null,
- "Altenahr": null,
- "Altenau(Bay)": null,
- "Altenbach": null,
- "Altenbamberg": null,
- "Altenbeken": null,
- "Altenberge": null,
- "Altenburg": null,
- "Altenerding": null,
- "Altenfeld(Rhön)": null,
- "Altenglan": null,
- "Altengörs": null,
- "Altenhasungen": null,
- "Altenkirchen(Westerwald)": null,
- "Altenmarkt(Alz)": null,
- "Altenseelbach": null,
- "Altenstadt(Hess)": null,
- "Altenstadt(Iller)": null,
- "Altenstadt(Waldnaab)": null,
- "Altenstadt-Höchst": null,
- "Altenstadt-Lindheim": null,
- "Altentreptow": null,
- "Altenwillershagen": null,
- "Altersbach": null,
- "Altes Lager": null,
- "Altglashütten-Falkau": null,
- "Althegnenberg": null,
- "Altheim(Hess)": null,
- "Althof": null,
- "Altingen(Württ)": null,
- "Altmittweida": null,
- "Altmorschen": null,
- "Altmügeln": null,
- "Altomünster": null,
- "Altoschatz-Rosenthal": null,
- "Altranft": null,
- "Altshausen": null,
- "Altstädten(Allgäu)": null,
- "Alttann": null,
- "Altötting": null,
- "Alveslohe": null,
- "Alzenau Burg": null,
- "Alzenau Nord": null,
- "Alzenau(Unterfr)": null,
- "Alzey": null,
- "Alzey Süd": null,
- "Alzey West": null,
- "Amberg": null,
- "Amerang": null,
- "Ammern": null,
- "Amorbach": null,
- "Ampfing": null,
- "Amsdorf": null,
- "Amstetten(W) Lokalbahn": null,
- "Amstetten(Württ)": null,
- "Amtshainersdorf": null,
- "Andernach": null,
- "Angermund": null,
- "Angermünde": null,
- "Angern-Rogätz": null,
- "Angersbach": null,
- "Angersdorf": null,
- "Anklam": null,
- "Annaberg-Buchholz Mitte": null,
- "Annaberg-Buchholz Süd": null,
- "Annaberg-Buchholz unterer Bf": null,
- "Annaburg": null,
- "Annweiler am Trifels": null,
- "Annweiler-Sarnstall": null,
- "Anrath": null,
- "Ansbach": null,
- "Antonsthal": null,
- "Anwanden": null,
- "Anzefahr": null,
- "Anzenkirchen": null,
- "Apensen": null,
- "Apolda": null,
- "Appenweier": null,
- "Appenweier": null,
- "Ardey": null,
- "Arensdorf(Köthen)": null,
- "Arenshausen": null,
- "Arfurt(Lahn)": null,
- "Armsheim": null,
- "Arnbach": null,
- "Arnsberg(Westf)": null,
- "Arnschwang": null,
- "Arnsdorf(Dresden)": null,
- "Arnstadt Hbf": null,
- "Arnstadt Süd": null,
- "Arrach": null,
- "Arsbeck": null,
- "Artern": null,
- "Arzberg(Oberfr)": null,
- "Aschaffenburg Hbf": null,
- "Aschaffenburg Hochschule": null,
- "Aschaffenburg Süd": null,
- "Aschau(Chiemgau)": null,
- "Ascheberg(Holst)": null,
- "Ascheberg(Westf)": null,
- "Aschendorf": null,
- "Aschersleben": null,
- "Ashausen": null,
- "Asperg": null,
- "Asselheim": null,
- "Assenheim(Oberhess)": null,
- "Assmannshausen": null,
- "Attendorn": null,
- "Attendorn-Hohen Hagen": null,
- "Au im Murgtal": null,
- "Au(Sieg)": null,
- "Aue(Sachs)": null,
- "Aue(Sachs) Erzgebirgsstadion": null,
- "Aue-Wingeshausen": null,
- "Auehütte": null,
- "Auerbach(V) ob Bf": null,
- "Auerbach(V) unt Bf": null,
- "Auerbach(Vogtl) Hp": null,
- "Auerbach(b Mosbach, Baden)": null,
- "Auerstedt": null,
- "Aufhausen(Württ)": null,
- "Aufhausen(b Erding)": null,
- "Auggen": null,
- "Augsburg Haunstetterstraße": null,
- "Augsburg Hbf": null,
- "Augsburg Messe": null,
- "Augsburg Morellstr.": null,
- "Augsburg-Hochzoll": null,
- "Augsburg-Hochzoll": null,
- "Augsburg-Oberhausen": null,
- "Augustfehn": null,
- "Aukrug": null,
- "Aulendorf": null,
- "Aumenau": null,
- "Aumühle": null,
- "Aumühle": null,
- "Auringen-Medenbach": null,
- "Auw an der Kyll": null,
- "Außenried": null,
- "Aying": null,
- "Aßlar": null,
- "Aßling(Oberbay)": null,
- "Baabe": null,
- "Baalberge": null,
- "Baar-Ebenhausen": null,
- "Babenhausen Langstadt": null,
- "Babenhausen(Hess)": null,
- "Babstadt": null,
- "Bacharach": null,
- "Bachern": null,
- "Bachfeld": null,
- "Bachheim": null,
- "Backnang": null,
- "Bad Abbach": null,
- "Bad Aibling": null,
- "Bad Aibling Kurpark": null,
- "Bad Arolsen": null,
- "Bad Bellingen": null,
- "Bad Belzig": null,
- "Bad Bentheim": null,
- "Bad Bergzabern": null,
- "Bad Berka": null,
- "Bad Berka Zeughausplatz": null,
- "Bad Berleburg": null,
- "Bad Bevensen": null,
- "Bad Birnbach": null,
- "Bad Blankenburg(Thüringerw)": null,
- "Bad Bodendorf": null,
- "Bad Bodenteich": null,
- "Bad Brambach": null,
- "Bad Bramstedt": null,
- "Bad Bramstedt Kurhaus": null,
- "Bad Breisig": null,
- "Bad Camberg": null,
- "Bad Doberan": null,
- "Bad Doberan Goethestraße": null,
- "Bad Doberan Stadtmitte": null,
- "Bad Driburg(Westf)": null,
- "Bad Düben(Mulde)": null,
- "Bad Dürkheim": null,
- "Bad Dürkheim-Trift": null,
- "Bad Dürrenberg": null,
- "Bad Elster": null,
- "Bad Empfing": null,
- "Bad Ems": null,
- "Bad Ems West": null,
- "Bad Endorf": null,
- "Bad Fallingbostel": null,
- "Bad Freienwalde": null,
- "Bad Friedrichshall Hbf": null,
- "Bad Friedrichshall-Kochendorf": null,
- "Bad Gandersheim": null,
- "Bad Griesbach(Schwarzwald)": null,
- "Bad Grönenbach": null,
- "Bad Harzburg": null,
- "Bad Herrenalb": null,
- "Bad Hersfeld": null,
- "Bad Homburg": null,
- "Bad Honnef Stadtbahn": null,
- "Bad Honnef(Rhein)": null,
- "Bad Höhenstadt": null,
- "Bad Hönningen": null,
- "Bad Imnau": null,
- "Bad Karlshafen": null,
- "Bad Kissingen": null,
- "Bad Kleinen": null,
- "Bad Kohlgrub": null,
- "Bad Kohlgrub Kurhaus": null,
- "Bad Kreuznach": null,
- "Bad Krozingen": null,
- "Bad Krozingen Ost": null,
- "Bad König": null,
- "Bad König Zell": null,
- "Bad Kösen": null,
- "Bad Köstritz": null,
- "Bad Kötzting": null,
- "Bad Laasphe": null,
- "Bad Laasphe-Niederlaasphe": null,
- "Bad Langensalza": null,
- "Bad Lausick": null,
- "Bad Lauterberg im Harz Barbis": null,
- "Bad Liebenwerda": null,
- "Bad Liebenzell": null,
- "Bad Lobenstein": null,
- "Bad Malente-Gremsmühlen": null,
- "Bad Mergentheim": null,
- "Bad Münder(Deister)": null,
- "Bad Münster a Stein": null,
- "Bad Münstereifel": null,
- "Bad Münstereifel-Arloff": null,
- "Bad Münstereifel-Iversheim": null,
- "Bad Nauheim": null,
- "Bad Nenndorf": null,
- "Bad Neuenahr": null,
- "Bad Neustadt(Saale)": null,
- "Bad Niedernau": null,
- "Bad Oeynhausen": null,
- "Bad Oeynhausen Süd": null,
- "Bad Oldesloe": null,
- "Bad Peterstal": null,
- "Bad Pyrmont": null,
- "Bad Rappenau": null,
- "Bad Rappenau Kurpark": null,
- "Bad Reichenhall": null,
- "Bad Reichenhall-Kirchberg": null,
- "Bad Rodach": null,
- "Bad Rotenfels Bf": null,
- "Bad Rotenfels Schloss": null,
- "Bad Rotenfels Weinbrennerstraße": null,
- "Bad Saarow": null,
- "Bad Saarow Klinikum": null,
- "Bad Sachsa": null,
- "Bad Salzdetfurth": null,
- "Bad Salzdetfurth Solebad": null,
- "Bad Salzhausen": null,
- "Bad Salzschlirf": null,
- "Bad Salzuflen": null,
- "Bad Salzungen": null,
- "Bad Sassendorf": null,
- "Bad Saulgau": null,
- "Bad Schandau": null,
- "Bad Schlema": null,
- "Bad Schmiedeberg": null,
- "Bad Schmiedeberg Kurzentrum": null,
- "Bad Schmiedeberg Süd": null,
- "Bad Schussenried": null,
- "Bad Schwartau": null,
- "Bad Schönborn Süd": null,
- "Bad Schönborn-Kronau": null,
- "Bad Sebastiansweiler-Belsen": null,
- "Bad Segeberg": null,
- "Bad Sobernheim": null,
- "Bad Soden(Taunus)": null,
- "Bad Soden-Salmünster": null,
- "Bad Sooden-Allendorf": null,
- "Bad St Peter Süd": null,
- "Bad St Peter-Ording": null,
- "Bad Staffelstein": null,
- "Bad Steben": null,
- "Bad Suderode": null,
- "Bad Sulza": null,
- "Bad Sulza Nord": null,
- "Bad Säckingen": null,
- "Bad Teinach-Neubulach": null,
- "Bad Tölz": null,
- "Bad Tönisstein": null,
- "Bad Urach": null,
- "Bad Urach Ermstalklinik": null,
- "Bad Urach Wasserfall": null,
- "Bad Vilbel": null,
- "Bad Vilbel Süd": null,
- "Bad Vilbel-Gronau": null,
- "Bad Waldsee": null,
- "Bad Wildbad Bf": null,
- "Bad Wildbad Kurpark": null,
- "Bad Wildbad Nord": null,
- "Bad Wildbad Uhlandplatz": null,
- "Bad Wildungen": null,
- "Bad Wilsnack": null,
- "Bad Wimpfen": null,
- "Bad Wimpfen Im Tal": null,
- "Bad Wimpfen-Hohenstadt": null,
- "Bad Windsheim": null,
- "Bad Wurzach": null,
- "Bad Wörishofen": null,
- "Bad Zwischenahn": null,
- "Baddeckenstedt": null,
- "Baden(Verden)": null,
- "Baden-Baden": null,
- "Baden-Baden Haueneberstein": null,
- "Baden-Baden Rebland": null,
- "Bagenz": null,
- "Bahlingen Riedlen": null,
- "Bahlingen am Kaiserstuhl": null,
- "Bahnbrücken": null,
- "Bahnsdorf": null,
- "Baierbrunn": null,
- "Baiersbronn Bf": null,
- "Baiersbronn Schule": null,
- "Baiersdorf": null,
- "Baitz": null,
- "Balbersdorf": null,
- "Baldham": null,
- "Balduinstein": null,
- "Balgheim": null,
- "Balgstädt": null,
- "Balingen Süd": null,
- "Balingen(Württ)": null,
- "Ballstädt(Gotha)": null,
- "Baltersweiler": null,
- "Balve": null,
- "Bamberg": null,
- "Bammental": null,
- "Bannemin-Mölschow": null,
- "Bansin Seebad": null,
- "Banteln": null,
- "Bantorf": null,
- "Barabein": null,
- "Barbelroth": null,
- "Bardowick": null,
- "Bardowick": null,
- "Bargstedt": null,
- "Bargteheide": null,
- "Barleben": null,
- "Barleber See": null,
- "Barmstedt": null,
- "Barmstedt Brunnenstr": null,
- "Barnstorf(Han)": null,
- "Barnten": null,
- "Barrien": null,
- "Barsinghausen": null,
- "Barth": null,
- "Barthmühle": null,
- "Baruth(Mark)": null,
- "Basdahl Kluste": null,
- "Basdahl Kluste": null,
- "Basdorf": null,
- "Basel Bad Bf": null,
- "Bassum": null,
- "Batzenhäusle": null,
- "Batzhausen": null,
- "Bauerbach": null,
- "Baumholder": null,
- "Baunach": null,
- "Baunatal-Guntershausen": null,
- "Baunatal-Rengershausen": null,
- "Bautzen": null,
- "Bavendorf": null,
- "Bayerbach": null,
- "Bayerisch Eisenstein": null,
- "Bayerisch Gmain": null,
- "Bayreuth Hbf": null,
- "Bayreuth-St Georgen": null,
- "Bayrischzell": null,
- "Bebitz": null,
- "Bebra": null,
- "Bechstedt-Trippstein": null,
- "Beckingen(Saar)": null,
- "Bedburg(Erft)": null,
- "Bedburg-Hau": null,
- "Beelen": null,
- "Beelitz Stadt": null,
- "Beelitz-Heilstätten": null,
- "Beerfelden Hetzbach": null,
- "Beeskow": null,
- "Beetz-Sommerfeld": null,
- "Behringersdorf": null,
- "Beienheim": null,
- "Beilrode": null,
- "Beimerstetten": null,
- "Beldorf": null,
- "Belgershain": null,
- "Belleben": null,
- "Bellenberg": null,
- "Bellheim Am Mühlbuckel": null,
- "Bellheim Bf": null,
- "Bempflingen": null,
- "Benediktbeuern": null,
- "Bengel": null,
- "Benneckenstein": null,
- "Bennemühlen": null,
- "Bennewitz": null,
- "Bennigsen": null,
- "Benningen(Neckar)": null,
- "Bennungen": null,
- "Benshausen": null,
- "Bensheim": null,
- "Bensheim-Auerbach": null,
- "Bentwisch": null,
- "Beratzhausen": null,
- "Berbisdorf": null,
- "Berbisdorf Anbau": null,
- "Berchtesgaden Hbf": null,
- "Berg(Pfalz)": null,
- "Berga(Elster)": null,
- "Berga-Kelbra": null,
- "Bergen auf Rügen": null,
- "Bergen(Oberbay)": null,
- "Bergenweiler": null,
- "Bergfelde(b Berlin)": null,
- "Berghausen Am Stadion": null,
- "Berghausen Pfinzbrücke": null,
- "Berghausen(Baden)": null,
- "Berghausen(Pfalz)": null,
- "Berghausen(b Wittgenstein)": null,
- "Bergheim(Erft)": null,
- "Bergisch Gladbach": null,
- "Bergsdorf": null,
- "Bergtheim": null,
- "Bergwitz": null,
- "Beringen Bad Bf": null,
- "Beringerfeld": null,
- "Beringhausen": null,
- "Beringstedt": null,
- "Berkenbrück": null,
- "Berlin Alexanderplatz": null,
- "Berlin Alexanderplatz (S)": null,
- "Berlin Alt-Reinickendorf": null,
- "Berlin Anhalter Bf": null,
- "Berlin Attilastr.": null,
- "Berlin Baumschulenweg": null,
- "Berlin Bellevue": null,
- "Berlin Betriebsbf Rummelsburg": null,
- "Berlin Betriebsbf Schöneweide": null,
- "Berlin Beusselstraße": null,
- "Berlin Bornholmer Str.": null,
- "Berlin Botanischer Garten": null,
- "Berlin Brandenburger Tor": null,
- "Berlin Buckower Chaussee": null,
- "Berlin Bundesplatz": null,
- "Berlin Eichborndamm": null,
- "Berlin Feuerbachstr.": null,
- "Berlin Frankfurter Allee": null,
- "Berlin Friedrichstraße": null,
- "Berlin Gehrenseestr.": null,
- "Berlin Gesundbrunnen": null,
- "Berlin Gesundbrunnen(S)": null,
- "Berlin Greifswalder Str": null,
- "Berlin Grünbergallee": null,
- "Berlin Hackescher Markt": null,
- "Berlin Hbf": null,
- "Berlin Hbf (S-Bahn)": null,
- "Berlin Hbf (tief)": null,
- "Berlin Heerstraße": null,
- "Berlin Heidelberger Platz": null,
- "Berlin Hermannstraße": null,
- "Berlin Hohenzollerndamm": null,
- "Berlin Humboldthain": null,
- "Berlin Innsbrucker Platz": null,
- "Berlin Jannowitzbrücke": null,
- "Berlin Julius-Leber-Brücke": null,
- "Berlin Jungfernheide": null,
- "Berlin Jungfernheide (S)": null,
- "Berlin Karl-Bonhoeffer-Nervenklinik": null,
- "Berlin Köllnische Heide": null,
- "Berlin Landsberger Allee": null,
- "Berlin Mehrower Allee": null,
- "Berlin Messe Nord/ICC (Witzleben)": null,
- "Berlin Messe Süd (Eichkamp)": null,
- "Berlin Mexikoplatz": null,
- "Berlin Nordbahnhof": null,
- "Berlin Nöldnerplatz": null,
- "Berlin Olympiastadion": null,
- "Berlin Oranienburger Straße": null,
- "Berlin Osdorfer Straße": null,
- "Berlin Ostbahnhof": null,
- "Berlin Ostkreuz": null,
- "Berlin Ostkreuz": null,
- "Berlin Ostkreuz (S)": null,
- "Berlin Ostkreuz (S)": null,
- "Berlin Ostkreuz (S)": null,
- "Berlin Plänterwald": null,
- "Berlin Poelchaustr.": null,
- "Berlin Potsdamer Platz": null,
- "Berlin Potsdamer Platz (S)": null,
- "Berlin Prenzlauer Allee": null,
- "Berlin Priesterweg": null,
- "Berlin Raoul-Wallenberg-Str.": null,
- "Berlin Rathaus Steglitz": null,
- "Berlin Savignyplatz": null,
- "Berlin Schichauweg": null,
- "Berlin Schönhauser Allee": null,
- "Berlin Sonnenallee": null,
- "Berlin Springpfuhl": null,
- "Berlin Storkower Str": null,
- "Berlin Sundgauer Str": null,
- "Berlin Südende": null,
- "Berlin Südkreuz": null,
- "Berlin Südkreuz (S)": null,
- "Berlin Südkreuz (S)": null,
- "Berlin Treptower Park": null,
- "Berlin Wannsee": null,
- "Berlin Wannsee": null,
- "Berlin Warschauer Straße": null,
- "Berlin Westend": null,
- "Berlin Westhafen": null,
- "Berlin Westkreuz": null,
- "Berlin Wollankstraße": null,
- "Berlin Wuhletal": null,
- "Berlin Yorckstr.(S1)": null,
- "Berlin Yorckstr.(S2)": null,
- "Berlin Zoologischer Garten": null,
- "Berlin Zoologischer Garten (S)": null,
- "Berlin-Adlershof": null,
- "Berlin-Altglienicke": null,
- "Berlin-Biesdorf": null,
- "Berlin-Blankenburg": null,
- "Berlin-Buch": null,
- "Berlin-Charlottenburg": null,
- "Berlin-Friedenau": null,
- "Berlin-Friedrichsfelde Ost": null,
- "Berlin-Friedrichshagen": null,
- "Berlin-Frohnau": null,
- "Berlin-Grunewald": null,
- "Berlin-Grünau": null,
- "Berlin-Halensee": null,
- "Berlin-Heiligensee": null,
- "Berlin-Hermsdorf": null,
- "Berlin-Hirschgarten": null,
- "Berlin-Hohenschönhausen": null,
- "Berlin-Karlshorst": null,
- "Berlin-Karow": null,
- "Berlin-Karow": null,
- "Berlin-Kaulsdorf": null,
- "Berlin-Köpenick": null,
- "Berlin-Lankwitz": null,
- "Berlin-Lichtenberg": null,
- "Berlin-Lichtenrade": null,
- "Berlin-Lichterfelde Ost": null,
- "Berlin-Lichterfelde Ost (S)": null,
- "Berlin-Lichterfelde Süd": null,
- "Berlin-Lichterfelde West": null,
- "Berlin-Mahlsdorf": null,
- "Berlin-Marienfelde": null,
- "Berlin-Marzahn": null,
- "Berlin-Neukölln": null,
- "Berlin-Neukölln": null,
- "Berlin-Nikolassee": null,
- "Berlin-Nikolassee": null,
- "Berlin-Oberspree": null,
- "Berlin-Pankow": null,
- "Berlin-Pankow-Heinersdorf": null,
- "Berlin-Pichelsberg": null,
- "Berlin-Rahnsdorf": null,
- "Berlin-Rummelsburg": null,
- "Berlin-Rummelsburg": null,
- "Berlin-Schlachtensee": null,
- "Berlin-Schulzendorf": null,
- "Berlin-Schöneberg": null,
- "Berlin-Schönefeld Flughafen": null,
- "Berlin-Schönefeld Flughafen": null,
- "Berlin-Schöneweide": null,
- "Berlin-Schöneweide (S)": null,
- "Berlin-Schönholz": null,
- "Berlin-Schönholz": null,
- "Berlin-Spandau": null,
- "Berlin-Spindlersfeld": null,
- "Berlin-Staaken": null,
- "Berlin-Stresow": null,
- "Berlin-Tegel (S)": null,
- "Berlin-Tempelhof": null,
- "Berlin-Tiergarten": null,
- "Berlin-Waidmannslust": null,
- "Berlin-Wartenberg": null,
- "Berlin-Wedding": null,
- "Berlin-Wilhelmshagen": null,
- "Berlin-Wilhelmsruh": null,
- "Berlin-Wittenau (Wilhelmsruher Damm)": null,
- "Berlin-Wuhlheide": null,
- "Berlin-Zehlendorf": null,
- "Bermatingen-Ahausen": null,
- "Bernau a Chiemsee": null,
- "Bernau(b Berlin)": null,
- "Bernau-Friedenstal": null,
- "Bernburg": null,
- "Bernburg-Friedenshall": null,
- "Bernburg-Strenzfeld": null,
- "Bernburg-Waldau": null,
- "Bernburg-Waldau": null,
- "Berne": null,
- "Bernried": null,
- "Bernterode": null,
- "Bersenbrück": null,
- "Berthelsdorf(Erzgeb)": null,
- "Berthelsdorf(Erzgebirge) Ort": null,
- "Bertsdorf": null,
- "Berzhahn": null,
- "Besch": null,
- "Besigheim": null,
- "Besseringen": null,
- "Bestensee": null,
- "Bestwig": null,
- "Bettmannsäge": null,
- "Betzdorf(Sieg)": null,
- "Beucha": null,
- "Beuchow": null,
- "Beuggen": null,
- "Beuna(Geiseltal)": null,
- "Beuren": null,
- "Beuron": null,
- "Beutelsbach": null,
- "Beutersitz": null,
- "Bexbach": null,
- "Beyendorf": null,
- "Bibelöd": null,
- "Biberach(Baden)": null,
- "Biberach(Riß)": null,
- "Biberach(Riß) Süd": null,
- "Biblis": null,
- "Bibra": null,
- "Bichl": null,
- "Bickenbach(Bergstr)": null,
- "Biebesheim": null,
- "Biedenkopf": null,
- "Biedenkopf-Schulzentrum": null,
- "Biederitz": null,
- "Bielefeld Hbf": null,
- "Bielefeld Ost": null,
- "Bielefeld-Senne": null,
- "Biendorf": null,
- "Bienenbüttel": null,
- "Bienenmühle": null,
- "Bierbach": null,
- "Bieren-Rödinghausen": null,
- "Bieringen": null,
- "Biersdorf(Westerw)": null,
- "Biersdorf-Ort(Ww)": null,
- "Biesenthal": null,
- "Biessenhofen": null,
- "Biessenhofen": null,
- "Bietigheim(Baden)": null,
- "Bietigheim-Bissingen": null,
- "Bietingen": null,
- "Bigge": null,
- "Bildstock": null,
- "Bilfingen": null,
- "Billenhausen": null,
- "Billerbeck": null,
- "Binau": null,
- "Bindfelde": null,
- "Bindlach": null,
- "Bingen(Rhein) Hbf": null,
- "Bingen(Rhein) Stadt": null,
- "Bingen-Gaulsheim": null,
- "Binolen": null,
- "Binsfeld": null,
- "Binz LB": null,
- "Binzen": null,
- "Birach": null,
- "Birkelbach": null,
- "Birkenau": null,
- "Birkenbringhausen": null,
- "Birkenfeld(Enz)": null,
- "Birkengrund": null,
- "Birkenmoor": null,
- "Birkenstein": null,
- "Birkenwerder(b Berlin)": null,
- "Birkungen": null,
- "Birresborn": null,
- "Bischheim-Gersdorf": null,
- "Bischofswerda": null,
- "Bischofswiesen": null,
- "Bischweier": null,
- "Bisingen": null,
- "Bissendorf": null,
- "Bitburg-Erdorf": null,
- "Bittelbronn": null,
- "Bitterfeld": null,
- "Bitzfeld": null,
- "Blaibach(Oberpf)": null,
- "Blaichach(Allgäu)": null,
- "Blankenbach": null,
- "Blankenberg(Meckl)": null,
- "Blankenberg(Sieg)": null,
- "Blankenburg(Harz)": null,
- "Blankenfelde(Teltow-Fläming)": null,
- "Blankenheim(Sangerhausen)": null,
- "Blankenheim(Wald)": null,
- "Blankenloch": null,
- "Blankensee(Meckl)": null,
- "Blankenstein(Saale)": null,
- "Blaubeuren": null,
- "Blaufelden": null,
- "Blaustein": null,
- "Blechhammer(Thür)": null,
- "Bleibach": null,
- "Bleichenbach(Oberh)": null,
- "Bleicherode Ost": null,
- "Blens": null,
- "Blieskastel-Lautzkirchen": null,
- "Blindheim": null,
- "Blumberg(b Berlin)": null,
- "Blumberg-Rehhahn": null,
- "Blumberg-Riedöschingen": null,
- "Blumberg-Zollhaus": null,
- "Blumenau": null,
- "Blumenberg": null,
- "Blumenhagen": null,
- "Blumenkamp": null,
- "Blumenthal(Mark)": null,
- "Blönsdorf": null,
- "Bobenheim": null,
- "Bobingen": null,
- "Bobitz": null,
- "Bobstadt": null,
- "Bocholt": null,
- "Bochum Hbf": null,
- "Bochum West": null,
- "Bochum-Dahlhausen": null,
- "Bochum-Ehrenfeld": null,
- "Bochum-Hamme": null,
- "Bochum-Langendreer": null,
- "Bochum-Langendreer": null,
- "Bochum-Langendreer West": null,
- "Bochum-Riemke": null,
- "Bockenheim-Kindenheim": null,
- "Bockum-Hövel": null,
- "Bodelsberg": null,
- "Bodelshausen": null,
- "Bodenburg": null,
- "Bodenfelde": null,
- "Bodenheim": null,
- "Bodenmais": null,
- "Bodenrode": null,
- "Bodenwöhr Nord": null,
- "Bogen": null,
- "Bohmte": null,
- "Boisheim": null,
- "Boizenburg(Elbe)": null,
- "Bokholt": null,
- "Bondorf(b Herrenberg)": null,
- "Bonn Brühler Str.": null,
- "Bonn UN Campus": null,
- "Bonn Hbf": null,
- "Bonn Hbf (tief)": null,
- "Bonn Helmholtzstraße": null,
- "Bonn Heussallee/Museumsmeile": null,
- "Bonn Konrad-Adenauer-Platz": null,
- "Bonn Stadthaus": null,
- "Bonn-Bad Godesberg": null,
- "Bonn-Bad Godesberg Stadthalle": null,
- "Bonn-Beuel": null,
- "Bonn-Duisdorf": null,
- "Bonn-Endenich Nord": null,
- "Bonn-Mehlem": null,
- "Bonn-Oberkassel": null,
- "Bonn-Oberkassel Mitte": null,
- "Bonn-Ramersdorf": null,
- "Bookholzberg": null,
- "Boostedt": null,
- "Bopfingen": null,
- "Boppard Hbf": null,
- "Boppard Süd": null,
- "Boppard-Bad Salzig": null,
- "Boppard-Buchholz": null,
- "Boppard-Fleckertshöhe": null,
- "Boppard-Hirzenach": null,
- "Bordesholm": null,
- "Borgeln": null,
- "Borgholzhausen": null,
- "Borgsdorf": null,
- "Bork(Westf)": null,
- "Borken(Hess)": null,
- "Borken(Westf)": null,
- "Borkheide": null,
- "Borna(Leipzig)": null,
- "Borsdorf(Hess)": null,
- "Borsdorf(Sachs)": null,
- "Bottrop Hbf": null,
- "Bottrop Hbf": null,
- "Bottrop-Boy": null,
- "Bottrop-Vonderort": null,
- "Bous(Saar)": null,
- "Boxberg-Wölchingen": null,
- "Brachbach": null,
- "Brachelen": null,
- "Brackwede": null,
- "Brahlstorf": null,
- "Brake(Unterweser)": null,
- "Brake(b Bielefeld)": null,
- "Brake(b Bielefeld)": null,
- "Brakel(Höxter)": null,
- "Bramsche": null,
- "Bramstedt(b Syke)": null,
- "Brand Tropical Islands": null,
- "Brandenburg Altstadt": null,
- "Brandenburg Hbf": null,
- "Brandoberndorf": null,
- "Brannenburg": null,
- "Braubach": null,
- "Braunsbedra": null,
- "Braunsbedra Ost": null,
- "Braunschweig Hbf": null,
- "Braunschweig-Gliesmarode": null,
- "Braunsdorf-Lichtenwalde": null,
- "Breddin": null,
- "Bredelar": null,
- "Bredenbek": null,
- "Bredstedt": null,
- "Brehna": null,
- "Breinig": null,
- "Breisach": null,
- "Breitenbrunn(Erzg)": null,
- "Breitenbrunn(Schwab)": null,
- "Breitendiel": null,
- "Breitendorf": null,
- "Breitengüßbach": null,
- "Breitscheidt(Altenkirchen, Ww)": null,
- "Breitungen(Werra)": null,
- "Bremen Hbf": null,
- "Bremen Kreinsloger": null,
- "Bremen Mühlenstraße": null,
- "Bremen Neustadt": null,
- "Bremen Turnerstraße": null,
- "Bremen-Aumund": null,
- "Bremen-Blumenthal": null,
- "Bremen-Burg": null,
- "Bremen-Farge": null,
- "Bremen-Hemelingen": null,
- "Bremen-Lesum": null,
- "Bremen-Mahndorf": null,
- "Bremen-Oberneuland": null,
- "Bremen-Oslebshausen": null,
- "Bremen-Schönebeck": null,
- "Bremen-Sebaldsbrück": null,
- "Bremen-St Magnus": null,
- "Bremen-Vegesack": null,
- "Bremen-Walle": null,
- "Bremerhaven Hbf": null,
- "Bremerhaven-Lehe": null,
- "Bremerhaven-Wulsdorf": null,
- "Bremervörde": null,
- "Brenk": null,
- "Brest-Aspe": null,
- "Breternitz": null,
- "Bretleben": null,
- "Bretten": null,
- "Bretten Kupferhälde": null,
- "Bretten Rechberg": null,
- "Bretten Schulzentrum": null,
- "Bretten Stadtmitte": null,
- "Bretten Wannenweg": null,
- "Bretten-Ruit": null,
- "Brettorf": null,
- "Bretzenheim(Nahe)": null,
- "Bretzfeld": null,
- "Breyell": null,
- "Brieselang": null,
- "Briesen(Mark)": null,
- "Brigachtal Kirchdorf": null,
- "Brigachtal Klengen": null,
- "Brilon Stadt": null,
- "Brilon Wald": null,
- "Britz": null,
- "Brocken": null,
- "Brockhöfe": null,
- "Broderstorf": null,
- "Brohl": null,
- "Brokstedt": null,
- "Bruchenbrücken": null,
- "Bruchhausen(b Ettlingen)": null,
- "Bruchköbel": null,
- "Bruchmühlbach-Miesau": null,
- "Bruchmühlen": null,
- "Bruchsal": null,
- "Bruchsal Am Mantel": null,
- "Bruchsal Bildungszentrum": null,
- "Bruchsal Schlachthof": null,
- "Bruchsal Schloßgarten": null,
- "Bruchsal Sportzentrum": null,
- "Bruchsal Stegwiesen": null,
- "Bruchsal Tunnelstr.": null,
- "Bruchweiler": null,
- "Bruckberg": null,
- "Brucken": null,
- "Bruckmühl": null,
- "Brunau-Packebusch": null,
- "Bräunlingen Bahnhof": null,
- "Bräunlingen Industriegebiet": null,
- "Brötzingen Mitte": null,
- "Brötzingen Sandweg": null,
- "Brötzingen Wohnlichstraße": null,
- "Brück(Mark)": null,
- "Brügge(Prign)": null,
- "Brügge(Westf)": null,
- "Brühl": null,
- "Brühl": null,
- "Brühl-Kierberg": null,
- "Bubach": null,
- "Bubenheim": null,
- "Bubenreuth": null,
- "Buchbrunn-Mainstockheim": null,
- "Buchen Ost": null,
- "Buchen(Odenw)": null,
- "Buchenau(Lahn)": null,
- "Buchenau(Oberbay)": null,
- "Buchenhain": null,
- "Buchenhorst": null,
- "Buchholz(Baden)": null,
- "Buchholz(Nordheide)": null,
- "Buchholz(Zauche)": null,
- "Buchloe": null,
- "Buckow(Beeskow)": null,
- "Buddenhagen": null,
- "Budenheim": null,
- "Bufleben": null,
- "Buggingen": null,
- "Buir": null,
- "Buldern": null,
- "Bullay(DB)": null,
- "Bundenthal-Rumbach": null,
- "Burbach Mitte": null,
- "Burbach(Kr Siegen)": null,
- "Burg Stargard(Meckl)": null,
- "Burg(Dillkr) Nord": null,
- "Burg(Dithm)": null,
- "Burg(Magdeburg)": null,
- "Burg-u. Nieder Gemünden": null,
- "Burgau(Schwab)": null,
- "Burgbernheim": null,
- "Burgbernheim-Wildbad": null,
- "Burgdorf": null,
- "Burghaun(Hünfeld)": null,
- "Burghausen": null,
- "Burgheim": null,
- "Burgholzhausen v d H": null,
- "Burgkemnitz": null,
- "Burgkirchen": null,
- "Burgkunstadt": null,
- "Burglauer": null,
- "Burgsinn": null,
- "Burgstall(Murr)": null,
- "Burgstädt": null,
- "Burgthann": null,
- "Burgweiler": null,
- "Burhafe(Ostfriesl)": null,
- "Burkhardswalde-Maxen": null,
- "Burkhardtsdorf": null,
- "Burkhardtsdorf Mitte": null,
- "Burkheim-Bischoffingen": null,
- "Burladingen": null,
- "Burladingen West": null,
- "Buschow": null,
- "Busenbach": null,
- "Busenberg-Schindhard": null,
- "Buttenheim": null,
- "Buttstädt": null,
- "Butzbach": null,
- "Buxtehude": null,
- "Bärenhecke-Johnsbach": null,
- "Bärenklau": null,
- "Bärenstein(Annaberg)": null,
- "Bärenstein(b Glashütte, Sachs)": null,
- "Bärnsdorf": null,
- "Bäumenheim": null,
- "Böbingen(Rems)": null,
- "Böblingen": null,
- "Böblingen Danziger Str": null,
- "Böblingen Heusteigstr": null,
- "Böblingen Südbf": null,
- "Böblingen Zimmerschlag": null,
- "Böbrach": null,
- "Böckingen Sonnenbrunnen": null,
- "Böckingen West": null,
- "Bödigheim": null,
- "Böhl-Iggelheim": null,
- "Böhlen Werke": null,
- "Böhlen(Leipzig)": null,
- "Böhmhof": null,
- "Böhringen-Rickelshausen": null,
- "Bölzke": null,
- "Bönen": null,
- "Bönningstedt": null,
- "Börnecke(Harz)": null,
- "Börßum": null,
- "Bösdorf(Sachs-Anh)": null,
- "Bösensell": null,
- "Bösperde": null,
- "Bötzingen": null,
- "Bötzingen Mühle": null,
- "Büchen": null,
- "Büchenbach": null,
- "Büches-Düdelsheim": null,
- "Bückeburg": null,
- "Büdingen(Oberhess)": null,
- "Büdingen(Westerw)": null,
- "Bühl(Baden)": null,
- "Bülzig": null,
- "Bünde(Westf)": null,
- "Bürgeln": null,
- "Bürstadt": null,
- "Bürstadt": null,
- "Büsenbachtal": null,
- "Büsum": null,
- "Büttgen": null,
- "Bützow": null,
- "Cadenberge": null,
- "Cadolzburg": null,
- "Cainsdorf": null,
- "Calau(Nl)": null,
- "Calbe(Saale) Ost": null,
- "Calbe(Saale) Stadt": null,
- "Calbe(Saale) West": null,
- "Calberlah": null,
- "Caldern": null,
- "Calmbach Bahnhof": null,
- "Calmbach Süd": null,
- "Calw": null,
- "Camburg(Saale)": null,
- "Cammin(Meckl)": null,
- "Capelle(Westf)": null,
- "Caputh Schwielowsee": null,
- "Caputh-Geltow": null,
- "Casekow": null,
- "Castrop-Rauxel Hbf": null,
- "Castrop-Rauxel Süd": null,
- "Castrop-Rauxel Süd": null,
- "Castrop-Rauxel-Merklinde": null,
- "Celle": null,
- "Cham(Oberpf)": null,
- "Chamerau": null,
- "Chemnitz Alt Chemnitz Center": null,
- "Chemnitz Annenstraße": null,
- "Chemnitz Brückenstraße/Freie Presse": null,
- "Chemnitz Erdmannsdorfer Straße": null,
- "Chemnitz Erfenschlag": null,
- "Chemnitz Friedrichstraße": null,
- "Chemnitz Gustav-Freytag-Straße": null,
- "Chemnitz Hbf": null,
- "Chemnitz Hbf (Bahnhofstraße)": null,
- "Chemnitz Kinderwaldstätte": null,
- "Chemnitz Mitte": null,
- "Chemnitz Moritzhof": null,
- "Chemnitz Omnibusbahnhof": null,
- "Chemnitz Riemenschneiderstraße": null,
- "Chemnitz Roter Turm": null,
- "Chemnitz Rösslerstraße": null,
- "Chemnitz Scheffelstraße": null,
- "Chemnitz Schneeberger Straße": null,
- "Chemnitz Schule Altchemnitz": null,
- "Chemnitz Stefan-Heym-Platz": null,
- "Chemnitz Süd": null,
- "Chemnitz Süd": null,
- "Chemnitz Theaterplatz": null,
- "Chemnitz Treffurthstraße": null,
- "Chemnitz Uhlestraße": null,
- "Chemnitz Zentralhaltestelle": null,
- "Chemnitz-Altchemnitz": null,
- "Chemnitz-Borna Hp": null,
- "Chemnitz-Borna Hp": null,
- "Chemnitz-Harthau": null,
- "Chemnitz-Hilbersdorf": null,
- "Chemnitz-Reichenhain": null,
- "Chemnitz-Schönau": null,
- "Chemnitz-Siegmar": null,
- "Chorin": null,
- "Clarholz": null,
- "Clausnitz": null,
- "Cloppenburg": null,
- "Coburg": null,
- "Coburg Nord": null,
- "Coburg-Neuses": null,
- "Cochem(Mosel)": null,
- "Coesfeld Schulzentrum": null,
- "Coesfeld(Westf)": null,
- "Collenberg": null,
- "Contwig": null,
- "Coppenbrügge": null,
- "Coschen": null,
- "Cossebaude": null,
- "Coswig(Anh)": null,
- "Coswig(b Dresden)": null,
- "Cottbus": null,
- "Cottbus-Merzdorf": null,
- "Cottbus-Merzdorf": null,
- "Cottbus-Sandow": null,
- "Cottbus-Willmersdorf Nord": null,
- "Crailsheim": null,
- "Cranzahl": null,
- "Creidlitz": null,
- "Creußen(Oberfr)": null,
- "Crimmitschau": null,
- "Crivitz": null,
- "Crossen Ort": null,
- "Crossen a d Elster": null,
- "Cunnertswalde": null,
- "Cursdorf": null,
- "Cuxhaven": null,
- "Cölbe": null,
- "Daaden": null,
- "Dabendorf": null,
- "Dachau Bahnhof": null,
- "Dachau Stadt": null,
- "Dachrieden": null,
- "Dachwig": null,
- "Dagebüll Kirche": null,
- "Dagebüll Mole": null,
- "Dahl": null,
- "Dahlbruch": null,
- "Dahlem(Eifel)": null,
- "Dahlen(Sachs)": null,
- "Dahlenburg": null,
- "Dahlerbrück": null,
- "Dahlewitz": null,
- "Dahn": null,
- "Dahn Süd": null,
- "Dalheim": null,
- "Dallau": null,
- "Dallgow-Döberitz": null,
- "Dannenberg Ost": null,
- "Dannenwalde(Gransee)": null,
- "Darching": null,
- "Darlingerode": null,
- "Darmstadt Hbf": null,
- "Darmstadt Nord": null,
- "Darmstadt Ost": null,
- "Darmstadt Süd": null,
- "Darmstadt TU-Lichtwiese": null,
- "Darmstadt-Arheilgen": null,
- "Darmstadt-Eberstadt": null,
- "Darmstadt-Kranichstein": null,
- "Darmstadt-Wixhausen": null,
- "Dasing": null,
- "Dattenfeld(Sieg)": null,
- "Dauenhof": null,
- "Daufenbach": null,
- "Dausenau": null,
- "Davensberg": null,
- "Dedenhausen": null,
- "Dedensen-Gümmer": null,
- "Dedinghausen": null,
- "Deezbüll": null,
- "Deggendorf Hbf": null,
- "Deidesheim": null,
- "Deining(Oberpf)": null,
- "Deinste": null,
- "Deinste": null,
- "Deisenhofen": null,
- "Deißlingen Mitte": null,
- "Delitzsch ob Bf": null,
- "Delitzsch unt Bf": null,
- "Dellfeld": null,
- "Dellfeld Ort": null,
- "Delmenhorst": null,
- "Demitz-Thumitz": null,
- "Demker": null,
- "Demmin": null,
- "Densborn": null,
- "Denzlingen": null,
- "Dernau": null,
- "Dernbach(Westerw)": null,
- "Derneburg(Han)": null,
- "Dessau Adria": null,
- "Dessau Hbf": null,
- "Dessau Süd": null,
- "Dessau-Alten": null,
- "Dessau-Mosigkau": null,
- "Dessau-Waldersee": null,
- "Detmold": null,
- "Dettelbach Bahnhof": null,
- "Dettenhausen": null,
- "Dettingen Freibad": null,
- "Dettingen Gsaidt": null,
- "Dettingen Lehen": null,
- "Dettingen(Main)": null,
- "Dettingen(Teck)": null,
- "Dettingen-Mitte": null,
- "Dettum": null,
- "Deuben(Zeitz)": null,
- "Deuerling": null,
- "Deuten": null,
- "Deutzen": null,
- "Diebach": null,
- "Dieburg": null,
- "Diedelsheim": null,
- "Diedorf(Schwab)": null,
- "Dienheim": null,
- "Diepholz": null,
- "Dieringhausen": null,
- "Dieskau": null,
- "Dietersheim": null,
- "Dietmannsried": null,
- "Dietzelbach": null,
- "Dietzenbach Bahnhof": null,
- "Dietzenbach Mitte": null,
- "Dietzenbach-Steinberg": null,
- "Dietzhausen": null,
- "Diez": null,
- "Diez Ost": null,
- "Dießen": null,
- "Dillbrecht": null,
- "Dillenburg": null,
- "Dillingen(Donau)": null,
- "Dillingen(Saar)": null,
- "Dingden": null,
- "Dingolfing": null,
- "Dinkelsbühl Bf": null,
- "Dinkelscherben": null,
- "Dinslaken": null,
- "Dippoldiswalde": null,
- "Dirmingen": null,
- "Dissen-Bad Rothenfelde": null,
- "Distelhausen": null,
- "Ditfurt": null,
- "Dittersbach": null,
- "Dittersdorf": null,
- "Dittigheim": null,
- "Ditzingen": null,
- "Doberlug-Kirchhain": null,
- "Doberlug-Kirchhain": null,
- "Doberschütz": null,
- "Dodendorf": null,
- "Dodenhof": null,
- "Dogern": null,
- "Dohna(Sachs)": null,
- "Dollbergen": null,
- "Dollern": null,
- "Dollnstein": null,
- "Dombühl": null,
- "Domnitz(Saalkr)": null,
- "Domsühl": null,
- "Donaueschingen": null,
- "Donaueschingen Allmendshofen": null,
- "Donaueschingen Aufen": null,
- "Donaueschingen Grüningen": null,
- "Donaueschingen Mitte/Siedlung": null,
- "Donauwörth": null,
- "Dorf Mecklenburg": null,
- "Dorfchemnitz": null,
- "Dorfen Bahnhof": null,
- "Dorfmark": null,
- "Dorfprozelten": null,
- "Dorheim(Wetterau)": null,
- "Dormagen": null,
- "Dormagen Chempark": null,
- "Dormagen Chempark": null,
- "Dornburg(Saale)": null,
- "Dornstetten": null,
- "Dorsten": null,
- "Dortelweil": null,
- "Dortmund Hbf": null,
- "Dortmund Hbf": null,
- "Dortmund Knappschaftskrankenhaus": null,
- "Dortmund Möllerbrücke": null,
- "Dortmund Signal Iduna Park": null,
- "Dortmund Stadthaus": null,
- "Dortmund Tierpark": null,
- "Dortmund Universität": null,
- "Dortmund West": null,
- "Dortmund-Aplerbeck": null,
- "Dortmund-Aplerbeck Süd": null,
- "Dortmund-Asseln Mitte": null,
- "Dortmund-Barop": null,
- "Dortmund-Brackel": null,
- "Dortmund-Bövinghausen": null,
- "Dortmund-Derne": null,
- "Dortmund-Dorstfeld": null,
- "Dortmund-Dorstfeld": null,
- "Dortmund-Dorstfeld": null,
- "Dortmund-Dorstfeld Süd": null,
- "Dortmund-Germania": null,
- "Dortmund-Huckarde": null,
- "Dortmund-Huckarde Nord": null,
- "Dortmund-Hörde": null,
- "Dortmund-Kirchderne": null,
- "Dortmund-Kirchhörde": null,
- "Dortmund-Kley": null,
- "Dortmund-Kruckel": null,
- "Dortmund-Kurl": null,
- "Dortmund-Körne": null,
- "Dortmund-Körne West": null,
- "Dortmund-Löttringhausen": null,
- "Dortmund-Lütgendortmund": null,
- "Dortmund-Lütgendortmund Nord": null,
- "Dortmund-Marten": null,
- "Dortmund-Marten Süd": null,
- "Dortmund-Mengede": null,
- "Dortmund-Nette/Oestrich": null,
- "Dortmund-Oespel": null,
- "Dortmund-Rahm": null,
- "Dortmund-Scharnhorst": null,
- "Dortmund-Somborn": null,
- "Dortmund-Sölde": null,
- "Dortmund-Westerfilde": null,
- "Dortmund-Wickede": null,
- "Dortmund-Wickede West": null,
- "Dortmund-Wischlingen": null,
- "Dorum(Weserm)": null,
- "Dossow(Prign)": null,
- "Dottenheim": null,
- "Dotternhausen-Dormettingen": null,
- "Drahnsdorf": null,
- "Drahtzug": null,
- "Drebkau": null,
- "Drei Annen Hohne": null,
- "Dreieich-Buchschlag": null,
- "Dreieich-Buchschlag": null,
- "Dreieich-Dreieichenhain": null,
- "Dreieich-Götzenhain": null,
- "Dreieich-Offenthal": null,
- "Dreieich-Sprendlingen": null,
- "Dreieich-Weibelfeld": null,
- "Dreikirchen": null,
- "Dreileben-Drackenstedt": null,
- "Drensteinfurt": null,
- "Dresden Bischofsplatz": null,
- "Dresden Flughafen": null,
- "Dresden Freiberger Straße": null,
- "Dresden Grenzstraße": null,
- "Dresden Hbf": null,
- "Dresden Industriegelände": null,
- "Dresden Mitte": null,
- "Dresden-Cotta": null,
- "Dresden-Dobritz": null,
- "Dresden-Friedrichstadt": null,
- "Dresden-Kemnitz": null,
- "Dresden-Klotzsche": null,
- "Dresden-Neustadt": null,
- "Dresden-Niedersedlitz": null,
- "Dresden-Pieschen": null,
- "Dresden-Plauen": null,
- "Dresden-Reick": null,
- "Dresden-Stetzsch": null,
- "Dresden-Strehlen": null,
- "Dresden-Trachau": null,
- "Dresden-Zschachwitz": null,
- "Dreye": null,
- "Drohndorf-Mehringen": null,
- "Ducherow": null,
- "Duckterath": null,
- "Dudweiler": null,
- "Duisburg Entenfang": null,
- "Duisburg Hbf": null,
- "Duisburg-Bissingheim": null,
- "Duisburg-Buchholz": null,
- "Duisburg-Großenbaum": null,
- "Duisburg-Hochfeld Süd": null,
- "Duisburg-Hochfeld Süd": null,
- "Duisburg-Meiderich Ost": null,
- "Duisburg-Meiderich Süd": null,
- "Duisburg-Obermeiderich": null,
- "Duisburg-Rahm": null,
- "Duisburg-Ruhrort": null,
- "Duisburg-Schlenk": null,
- "Duisburg-Wedau": null,
- "Duisburg-Wedau": null,
- "Durach": null,
- "Durmersheim": null,
- "Durmersheim Nord": null,
- "Dutenhofen(Wetzlar)": null,
- "Dußlingen": null,
- "Döbeln Hbf": null,
- "Döberitz": null,
- "Döggingen": null,
- "Döhlau": null,
- "Döllstädt": null,
- "Dörfles-Esbach": null,
- "Dörpen": null,
- "Dörrberg": null,
- "Dörverden": null,
- "Dülken": null,
- "Dülmen": null,
- "Dülmen": null,
- "Düren": null,
- "Düren": null,
- "Düren Annakirmesplatz": null,
- "Düren Im Großen Tal": null,
- "Düren Renkerstraße": null,
- "Düren-Kuhbrücke": null,
- "Düren-Lendersdorf": null,
- "Dürrenbüchig": null,
- "Dürrenwaid Bahnhof": null,
- "Dürrnhaar": null,
- "Dürrröhrsdorf": null,
- "Düsseldorf Flughafen": null,
- "Düsseldorf Flughafen": null,
- "Düsseldorf Flughafen Terminal": null,
- "Düsseldorf Friedrichstadt": null,
- "Düsseldorf Hbf": null,
- "Düsseldorf Volksgarten": null,
- "Düsseldorf Völklinger Str.": null,
- "Düsseldorf Wehrhahn": null,
- "Düsseldorf-Benrath": null,
- "Düsseldorf-Bilk": null,
- "Düsseldorf-Derendorf": null,
- "Düsseldorf-Derendorf": null,
- "Düsseldorf-Eller": null,
- "Düsseldorf-Eller Mitte": null,
- "Düsseldorf-Eller Süd": null,
- "Düsseldorf-Flingern": null,
- "Düsseldorf-Garath": null,
- "Düsseldorf-Gerresheim": null,
- "Düsseldorf-Hamm": null,
- "Düsseldorf-Hellerhof": null,
- "Düsseldorf-Oberbilk": null,
- "Düsseldorf-Rath": null,
- "Düsseldorf-Rath Mitte": null,
- "Düsseldorf-Reisholz": null,
- "Düsseldorf-Unterrath": null,
- "Düsseldorf-Zoo": null,
- "Dütschow": null,
- "Ebelsbach-Eltmann": null,
- "Ebenhausen(Unterfr)": null,
- "Ebenhausen-Schäftlarn": null,
- "Ebenhofen": null,
- "Ebensfeld": null,
- "Eberbach": null,
- "Ebermannstadt": null,
- "Ebermergen": null,
- "Ebern": null,
- "Ebersbach(Fils)": null,
- "Ebersbach(Sachs)": null,
- "Ebersberg(Oberbay)": null,
- "Ebersbrunn": null,
- "Ebersdorf(b Coburg)": null,
- "Eberswalde Hbf": null,
- "Ebertsheim": null,
- "Ebing": null,
- "Ebringen": null,
- "Ebstorf(Uelzen)": null,
- "Echem": null,
- "Eching": null,
- "Echterdingen": null,
- "Echzell": null,
- "Eckardtsleben": null,
- "Eckartsberga(Thür)": null,
- "Eckartshausen-Ilshofen": null,
- "Eckernförde": null,
- "Eckersmühlen": null,
- "Eddersheim": null,
- "Edelfingen": null,
- "Edenkoben": null,
- "Edermünde-Grifte": null,
- "Edesheim(Pfalz)": null,
- "Ediger-Eller": null,
- "Edingen(Wetzlar)": null,
- "Edle Krone": null,
- "Edling": null,
- "Effelder(Thür)": null,
- "Effolderbach": null,
- "Efringen-Kirchen": null,
- "Egelsbach": null,
- "Egersdorf": null,
- "Egestorf(Deister)": null,
- "Eggenfelden": null,
- "Eggenfelden Mitte": null,
- "Eggenstein Bf": null,
- "Eggersdorf": null,
- "Eggesin": null,
- "Eggingen": null,
- "Egglkofen": null,
- "Eggmühl": null,
- "Eggolsheim": null,
- "Eglharting": null,
- "Egling": null,
- "Ehingen(Donau)": null,
- "Ehlenbruch": null,
- "Ehlershausen": null,
- "Ehningen(b Böblingen)": null,
- "Ehr": null,
- "Ehrang": null,
- "Ehrang Ort": null,
- "Ehringen": null,
- "Ehringhausen(Kr Lippstadt)": null,
- "Ehringshausen(Kr Wetzlar)": null,
- "Ehringshausen(Oberhess)": null,
- "Eibau": null,
- "Eichen(Kr Siegen)": null,
- "Eichenau(Oberbay)": null,
- "Eichenberg": null,
- "Eichenzell": null,
- "Eichhagen": null,
- "Eicholzheim": null,
- "Eichstedt(Altm)": null,
- "Eichstetten am Kaiserstuhl": null,
- "Eichstätt Bahnhof": null,
- "Eichstätt Stadt": null,
- "Eichwalde": null,
- "Eickendorf": null,
- "Eilenburg": null,
- "Eilenburg Ost": null,
- "Eilenburg Ost": null,
- "Eilendorf": null,
- "Eilsleben(b Magdeburg)": null,
- "Eilvese": null,
- "Eimeldingen": null,
- "Einbeck Salzderhelden": null,
- "Einfeld": null,
- "Einsiedel": null,
- "Einsiedel Hp Gymnasium": null,
- "Einsiedlerhof": null,
- "Einöd(Saar)": null,
- "Eisenach": null,
- "Eisenach Opelwerke Hp": null,
- "Eisenach West": null,
- "Eisenbach-Matzenbach": null,
- "Eisenberg(Pfalz)": null,
- "Eisenberg(Pfalz)": null,
- "Eisenberg(Pfalz)": null,
- "Eisenheim": null,
- "Eisenhüttenstadt": null,
- "Eisenärzt": null,
- "Eiserfeld(Sieg)": null,
- "Eisfeld": null,
- "Eisfelder Talmühle": null,
- "Eislingen(Fils)": null,
- "Eiswoog": null,
- "Eitensheim": null,
- "Eitorf": null,
- "Elend": null,
- "Elfershausen-Trimberg": null,
- "Elgersburg": null,
- "Ellefeld": null,
- "Ellental": null,
- "Ellerau": null,
- "Ellhofen": null,
- "Ellingen(Bay)": null,
- "Ellrich": null,
- "Ellwangen": null,
- "Ellzee": null,
- "Elmenhorst": null,
- "Elmshorn": null,
- "Elpersheim": null,
- "Elsfleth": null,
- "Elsholz": null,
- "Elsnigk(Anh)": null,
- "Elstal": null,
- "Elster(Elbe)": null,
- "Elsterberg": null,
- "Elsterberg-Kunstseidenwerk": null,
- "Elsterwerda": null,
- "Elsterwerda-Biehla": null,
- "Eltersdorf": null,
- "Eltville": null,
- "Elxleben": null,
- "Elz(Limburg/Lahn)": null,
- "Elz(Limburg/Lahn) Süd": null,
- "Elzach": null,
- "Elze(Han)": null,
- "Emden Außenhafen": null,
- "Emden Hbf": null,
- "Emmelshausen": null,
- "Emmendingen": null,
- "Emmerich": null,
- "Emmerke": null,
- "Emmerthal": null,
- "Empel-Rees": null,
- "Empelde": null,
- "Emsdetten": null,
- "Emskirchen": null,
- "Endersbach": null,
- "Endingen am Kaiserstuhl": null,
- "Endingen(Württ)": null,
- "Engeln": null,
- "Engelskirchen": null,
- "Engen": null,
- "Engers": null,
- "Engertsham": null,
- "Engstingen": null,
- "Engstlatt": null,
- "Enkenbach": null,
- "Ennepetal": null,
- "Ensdorf(Saar)": null,
- "Enspel": null,
- "Entringen": null,
- "Enzberg": null,
- "Enzisweiler": null,
- "Epe(Westf)": null,
- "Eppelborn": null,
- "Eppelsheim(Rheinhess)": null,
- "Eppertshausen": null,
- "Eppingen": null,
- "Eppingen West": null,
- "Eppstein": null,
- "Eppstein-Bremthal": null,
- "Erbach(Odenw)": null,
- "Erbach(Odenw) Nord": null,
- "Erbach(Rheingau)": null,
- "Erbach(Württ)": null,
- "Erdeborn": null,
- "Erding": null,
- "Erdmannhausen": null,
- "Erdmannsdorf-Augustusburg": null,
- "Erdweg": null,
- "Erftstadt": null,
- "Erfurt Hbf": null,
- "Erfurt Nord": null,
- "Erfurt Ost": null,
- "Erfurt-Bischleben": null,
- "Erfurt-Gispersleben": null,
- "Ergenzingen": null,
- "Ergoldsbach": null,
- "Ergste": null,
- "Eriskirch": null,
- "Erkelenz": null,
- "Erkersreuth": null,
- "Erkner": null,
- "Erkrath": null,
- "Erkrath-Nord": null,
- "Erla": null,
- "Erlabrunn(Erzgeb)": null,
- "Erlangen": null,
- "Erlangen Paul-Gossen-Straße": null,
- "Erlangen-Bruck": null,
- "Erlau(Sachs)": null,
- "Erlenbach(Main)": null,
- "Erndtebrück": null,
- "Ernsgaden": null,
- "Ernsthausen": null,
- "Ernstthal am Rennsteig": null,
- "Erpel(Rhein)": null,
- "Erpolzheim": null,
- "Ersingen": null,
- "Ersingen West": null,
- "Erzhausen": null,
- "Erzingen(Baden)": null,
- "Erzingen(Württ)": null,
- "Eschborn": null,
- "Eschborn Süd": null,
- "Eschede": null,
- "Eschelbronn": null,
- "Eschenau(Mittelfr)": null,
- "Eschenau(b Heilbronn)": null,
- "Eschenbach(b Markt Erlbach)": null,
- "Eschenlohe": null,
- "Escherndorf-Vogelburg": null,
- "Eschhofen": null,
- "Eschwege": null,
- "Eschwege-Niederhone": null,
- "Eschweiler Hbf": null,
- "Eschweiler Talbahnhof": null,
- "Eschweiler-Nothberg": null,
- "Eschweiler-St.Jöris": null,
- "Eschweiler-Weisweiler": null,
- "Eschweiler-West": null,
- "Esens(Ostfriesl)": null,
- "Espelkamp": null,
- "Espenau-Mönchehof": null,
- "Essen Hbf": null,
- "Essen Stadtwald": null,
- "Essen Süd": null,
- "Essen West": null,
- "Essen(Oldb)": null,
- "Essen-Altenessen": null,
- "Essen-Bergeborbeck": null,
- "Essen-Borbeck": null,
- "Essen-Borbeck Süd": null,
- "Essen-Dellwig": null,
- "Essen-Dellwig Ost": null,
- "Essen-Eiberg": null,
- "Essen-Frohnhausen": null,
- "Essen-Gerschede": null,
- "Essen-Holthausen": null,
- "Essen-Horst": null,
- "Essen-Hügel": null,
- "Essen-Kray Nord": null,
- "Essen-Kray Süd": null,
- "Essen-Kupferdreh": null,
- "Essen-Steele": null,
- "Essen-Steele Ost": null,
- "Essen-Werden": null,
- "Essen-Zollverein Nord": null,
- "Essen-Überruhr": null,
- "Esslingen(Neckar)": null,
- "Esslingen-Mettingen": null,
- "Esslingen-Zell": null,
- "Esting": null,
- "Etelsen": null,
- "Ettenhausen": null,
- "Etterzhausen": null,
- "Ettlingen Stadt": null,
- "Ettlingen West": null,
- "Etzbach": null,
- "Etzelwang": null,
- "Etzenbach": null,
- "Etzenricht": null,
- "Etzenrot": null,
- "Etzleben": null,
- "Eubigheim": null,
- "Euerdorf": null,
- "Euskirchen": null,
- "Euskirchen Zuckerfabrik": null,
- "Euskirchen-Großbüllesheim": null,
- "Euskirchen-Kreuzweingarten": null,
- "Euskirchen-Kuchenheim": null,
- "Euskirchen-Stotzheim": null,
- "Eutin": null,
- "Eutingen im Gäu": null,
- "Eutingen(Baden)": null,
- "Eutzsch": null,
- "Eyach": null,
- "Eystrup": null,
- "Eßleben": null,
- "Fachingen(Lahn)": null,
- "Fahrenkrug": null,
- "Fahrnau": null,
- "Falkenau(Sachs)Hp": null,
- "Falkenau(Sachs)Süd": null,
- "Falkenberg(Elster)": null,
- "Falkenberg(Elster)": null,
- "Falkenberg(Elster)": null,
- "Falkenberg(Mark)": null,
- "Falkenhagen Gewerbepark Prignitz": null,
- "Falkensee": null,
- "Falkenstein(Vogtl)": null,
- "Fallersleben": null,
- "Fangschleuse": null,
- "Farchant": null,
- "Fasanenpark": null,
- "Faulbach(Main)": null,
- "Faurndau": null,
- "Favoritepark": null,
- "Fehmarn-Burg": null,
- "Feilitzsch": null,
- "Feldafing": null,
- "Feldberg-Bärental": null,
- "Felde": null,
- "Feldhausen": null,
- "Feldkirchen(b München)": null,
- "Fellbach": null,
- "Felsberg-Altenbrunslar": null,
- "Felsberg-Gensungen": null,
- "Felsberg-Wolfershausen": null,
- "Ferch-Lienewitz": null,
- "Ferdinandshof": null,
- "Fermerswalde": null,
- "Ferndorf(Siegen)": null,
- "Feucht": null,
- "Feucht Ost": null,
- "Feucht-Moosbach": null,
- "Feuchtwangen Bf": null,
- "Feudingen": null,
- "Fichtenberg": null,
- "Filderstadt": null,
- "Filsen": null,
- "Finkenheerd": null,
- "Finkenkrug": null,
- "Finnentrop": null,
- "Finningerstraße": null,
- "Finsterwalde(Niederlausitz)": null,
- "Fischbach(Nürnberg)": null,
- "Fischbach-Camphausen": null,
- "Fischbach-Weierbach": null,
- "Fischbachau": null,
- "Fischen": null,
- "Fischhaus": null,
- "Fischhausen-Neuhaus": null,
- "Fladungen": null,
- "Flechtingen": null,
- "Fleetmark": null,
- "Flehingen": null,
- "Flensburg": null,
- "Flieden": null,
- "Flintbek": null,
- "Flintsbach": null,
- "Flomersheim": null,
- "Floßmühle": null,
- "Flöha": null,
- "Flöha-Plaue": null,
- "Flörsheim(Main)": null,
- "Fohrde": null,
- "Forbach(Schwarzw)": null,
- "Forchheim(Oberfr)": null,
- "Forchheim(b Karlsruhe)": null,
- "Fornsbach": null,
- "Forst(Lausitz)": null,
- "Forsthaus": null,
- "Forsting": null,
- "Forth": null,
- "Frahelsbruck": null,
- "Frankenberg(Eder)": null,
- "Frankenberg(Sachs)": null,
- "Frankenberg(Sachs) Süd": null,
- "Frankenberg-Goßberg": null,
- "Frankenberg-Viermünden": null,
- "Frankenstein(Pfalz)": null,
- "Frankenstein(Sachs)": null,
- "Frankenthal Hbf": null,
- "Frankenthal Süd": null,
- "Frankfurt Hbf (tief)": null,
- "Frankfurt am Main - Stadion": null,
- "Frankfurt(M) Flughafen Fernbf": null,
- "Frankfurt(M) Flughafen Regionalbf": null,
- "Frankfurt(M)Galluswarte": null,
- "Frankfurt(M)Hauptwache": null,
- "Frankfurt(M)Konstablerwache": null,
- "Frankfurt(M)Lokalbahnhof": null,
- "Frankfurt(M)Mühlberg": null,
- "Frankfurt(M)Ostendstraße": null,
- "Frankfurt(M)Stresemannallee": null,
- "Frankfurt(M)Taunusanlage": null,
- "Frankfurt(Main)Hbf": null,
- "Frankfurt(Main)Messe": null,
- "Frankfurt(Main)Ost": null,
- "Frankfurt(Main)Süd": null,
- "Frankfurt(Main)West": null,
- "Frankfurt(Oder)": null,
- "Frankfurt(Oder)-Neuberesinchen": null,
- "Frankfurt(Oder)-Rosengarten": null,
- "Frankfurt-Berkersheim": null,
- "Frankfurt-Eschersheim": null,
- "Frankfurt-Frankfurter Berg": null,
- "Frankfurt-Griesheim": null,
- "Frankfurt-Höchst": null,
- "Frankfurt-Höchst Farbwerke": null,
- "Frankfurt-Louisa": null,
- "Frankfurt-Louisa": null,
- "Frankfurt-Mainkur": null,
- "Frankfurt-Nied": null,
- "Frankfurt-Niederrad": null,
- "Frankfurt-Niederrad": null,
- "Frankfurt-Rödelheim": null,
- "Frankfurt-Sindlingen": null,
- "Frankfurt-Sindlingen": null,
- "Frankfurt-Sossenheim": null,
- "Frankfurt-Unterliederbach": null,
- "Frankfurt-Zeilsheim": null,
- "Frankleben": null,
- "Frauenalb-Schielberg": null,
- "Frauenau": null,
- "Frauenhain": null,
- "Frechen-Königsdorf": null,
- "Freckleben": null,
- "Freden(Leine)": null,
- "Fredersdorf(b Berlin)": null,
- "Freiberg(Neckar)": null,
- "Freiberg(Sachs)": null,
- "Freiburg Klinikum": null,
- "Freiburg Messe/Universität": null,
- "Freiburg West": null,
- "Freiburg(Breisgau) Hbf": null,
- "Freiburg-Herdern": null,
- "Freiburg-Littenweiler": null,
- "Freiburg-St Georgen": null,
- "Freiburg-Wiehre": null,
- "Freiburg-Zähringen": null,
- "Freienohl": null,
- "Freienorla": null,
- "Freihalden": null,
- "Freihung": null,
- "Freihöls": null,
- "Freilassing": null,
- "Freilassing-Hofham": null,
- "Freimersheim(Rheinh)": null,
- "Freinsheim": null,
- "Freising": null,
- "Freital-Coßmannsdorf": null,
- "Freital-Deuben": null,
- "Freital-Hainsberg": null,
- "Freital-Hainsberg": null,
- "Freital-Hainsberg West": null,
- "Freital-Potschappel": null,
- "Frellstedt": null,
- "Frelsdorf": null,
- "Fremdingen Bf": null,
- "Fremersdorf": null,
- "Frenz": null,
- "Fresenburg": null,
- "Fretzdorf": null,
- "Freudenberg-Kirschfurt": null,
- "Freudenstadt Hbf": null,
- "Freudenstadt Industriegebiet": null,
- "Freudenstadt Schulzentrum": null,
- "Freudenstadt Stadt": null,
- "Freusburg Siedlung": null,
- "Freyburg(Unstrut)": null,
- "Freyung Bf": null,
- "Frickenhausen": null,
- "Frickenhausen Kelterstraße": null,
- "Frickhofen": null,
- "Fridingen(b Tuttlingen)": null,
- "Fridolfing": null,
- "Friedberg Süd": null,
- "Friedberg(Augsburg)": null,
- "Friedberg(Hess)": null,
- "Friedelhausen": null,
- "Friedensdorf(Lahn)": null,
- "Friedersdorf(Königs Wusterhausen)": null,
- "Friedewald(Kr Dresden) Bad": null,
- "Friedewald(Kr Dresden)Hp": null,
- "Friedland(Han)": null,
- "Friedrich Wilhelmshütte": null,
- "Friedrichroda": null,
- "Friedrichsdorf(Taunus)": null,
- "Friedrichsfeld(Niederrhein)": null,
- "Friedrichsgabe": null,
- "Friedrichshafen Flughafen": null,
- "Friedrichshafen Hafen": null,
- "Friedrichshafen Landratsamt": null,
- "Friedrichshafen Ost": null,
- "Friedrichshafen Stadt": null,
- "Friedrichshafen-Fischbach": null,
- "Friedrichshafen-Kluftern": null,
- "Friedrichshafen-Manzell": null,
- "Friedrichshöhe": null,
- "Friedrichsruh": null,
- "Friedrichsruhe(Meck)": null,
- "Friedrichssegen": null,
- "Friedrichstadt": null,
- "Friedrichstal b Freudenstadt": null,
- "Friedrichstal(Baden)": null,
- "Friedrichsthal(Saar)": null,
- "Friedrichsthal(Saar) Mitte": null,
- "Friedrichsthal(b Bayreuth)": null,
- "Friesack(Mark)": null,
- "Friesenheim(Baden)": null,
- "Frimmersdorf": null,
- "Fritzlar": null,
- "Frohburg": null,
- "Frommern": null,
- "Fronhausen(Lahn)": null,
- "Frose": null,
- "Frömern": null,
- "Fröndenberg": null,
- "Fröttstädt": null,
- "Fulda": null,
- "Fuldatal-Ihringshausen": null,
- "Furschenbach": null,
- "Furth i Wald": null,
- "Furth(b Deisenhofen)": null,
- "Fährbrücke": null,
- "Föhren": null,
- "Förbau": null,
- "Förderstedt": null,
- "Förtha(Eisenach)": null,
- "Förtschendorf": null,
- "Fürfurt": null,
- "Fürstenberg(Havel)": null,
- "Fürsteneck": null,
- "Fürstenfeldbruck": null,
- "Fürstenwald": null,
- "Fürstenwalde Süd": null,
- "Fürstenwalde(Spree)": null,
- "Fürstenzell": null,
- "Fürth Westvorstadt": null,
- "Fürth(Bay)Hbf": null,
- "Fürth(Odenw)": null,
- "Fürth-Burgfarrnbach": null,
- "Fürth-Dambach": null,
- "Fürth-Unterfarrnbach": null,
- "Fürth-Unterfarrnbach": null,
- "Fürth-Unterfürberg": null,
- "Füssen": null,
- "Gablingen": null,
- "Gadebusch": null,
- "Gaggenau Bf": null,
- "Gaggenau Mercedes-Benz Werk": null,
- "Gaildorf West": null,
- "Gaimersheim": null,
- "Gaißach": null,
- "Galgenschanze": null,
- "Gamburg(Tauber)": null,
- "Gammertingen": null,
- "Gammertingen Europastraße": null,
- "Ganderkesee": null,
- "Gangloffsömmern": null,
- "Garbeck": null,
- "Garbenteich": null,
- "Garching(Alz)": null,
- "Gardelegen": null,
- "Garding": null,
- "Garftitz": null,
- "Garmisch-Partenkirchen": null,
- "Garmisch-Partenkirchen Hausberg": null,
- "Gars(Inn)": null,
- "Gatersleben": null,
- "Gau Algesheim": null,
- "Gau Bickelheim": null,
- "Gaubüttelbrunn": null,
- "Gausbach": null,
- "Gauselfingen": null,
- "Gauting": null,
- "Gebra(Hainleite)": null,
- "Geeste": null,
- "Geestenseth": null,
- "Geestgottberg": null,
- "Gehlberg": null,
- "Geigant": null,
- "Geilenkirchen": null,
- "Geilenkirchen": null,
- "Geilhausen": null,
- "Geiselhöring": null,
- "Geisenbrunn": null,
- "Geisenhausen": null,
- "Geisenheim": null,
- "Geising": null,
- "Geisingen": null,
- "Geisingen-Aulfingen": null,
- "Geisingen-Hausen": null,
- "Geisingen-Kirchen": null,
- "Geisingen-Leipferdingen": null,
- "Geislingen(Steige)": null,
- "Geislingen(Steige)West": null,
- "Geitau": null,
- "Geithain": null,
- "Gelbensande": null,
- "Geldern": null,
- "Gelnhausen": null,
- "Gelsenkirchen Hbf": null,
- "Gelsenkirchen Zoo": null,
- "Gelsenkirchen-Buer Nord": null,
- "Gelsenkirchen-Buer Süd": null,
- "Gelsenkirchen-Hassel": null,
- "Gelsenkirchen-Rotthausen": null,
- "Geltendorf": null,
- "Gemmingen": null,
- "Gemmingen West": null,
- "Gemünden(Main)": null,
- "Genderkingen": null,
- "Gendorf": null,
- "Gengenbach": null,
- "Gennweiler": null,
- "Gensingen-Horrweiler": null,
- "Genthin": null,
- "Georgensgmünd": null,
- "Gera Hbf": null,
- "Gera Ost": null,
- "Gera Süd": null,
- "Gera-Langenberg": null,
- "Gera-Liebschwitz": null,
- "Gera-Zwötzen": null,
- "Geraberg": null,
- "Geradstetten": null,
- "Gerhausen": null,
- "Gerichshain": null,
- "Gerlachsheim": null,
- "Gerlenhofen": null,
- "Germering-Unterpfaffenhofen": null,
- "Germersheim Bahnhof": null,
- "Germersheim Mitte/Rhein": null,
- "Germersheim Süd/Nolte": null,
- "Gernlinden": null,
- "Gernrode(Harz)": null,
- "Gernrode-Niederorschel": null,
- "Gernsbach Bf": null,
- "Gernsbach Mitte": null,
- "Gernsheim": null,
- "Geroldshausen": null,
- "Gerolstein": null,
- "Gersdorf(Görlitz)": null,
- "Gersfeld(Rhön)": null,
- "Gerstetten": null,
- "Gersthofen": null,
- "Gerstungen": null,
- "Gertenbach": null,
- "Gerwisch": null,
- "Geseke": null,
- "Gessertshausen": null,
- "Gettenau-Bingenheim": null,
- "Gettorf": null,
- "Gevelsberg Hbf": null,
- "Gevelsberg West": null,
- "Gevelsberg-Kipp": null,
- "Gevelsberg-Knapp": null,
- "Giengen(Brenz)": null,
- "Giersleben": null,
- "Gießen": null,
- "Gießen Erdkauter Weg": null,
- "Gießen Licher Str": null,
- "Gießen Oswaldsgarten": null,
- "Gifhorn": null,
- "Gifhorn Stadt": null,
- "Gilching-Argelsried": null,
- "Gingen(Fils)": null,
- "Girod": null,
- "Gittelde/Bad Grund(Harz)": null,
- "Gladbeck Ost": null,
- "Gladbeck West": null,
- "Gladbeck-Zweckel": null,
- "Glan-Münchweiler": null,
- "Glanzstoffwerke": null,
- "Glashütte(Sachs)": null,
- "Glaubitz(Riesa)": null,
- "Glauburg-Glauberg": null,
- "Glauburg-Stockheim": null,
- "Glauchau(Sachs)": null,
- "Glauchau-Schönbörnchen": null,
- "Glesch": null,
- "Glossen (b Oschatz)": null,
- "Glöwen": null,
- "Glückauf": null,
- "Glückstadt": null,
- "Gmund(Tegernsee)": null,
- "Gnadau": null,
- "Gnarrenburg": null,
- "Gnarrenburg Nord": null,
- "Gnevkow": null,
- "Goch": null,
- "Gochsheim(Baden)": null,
- "Godelheim": null,
- "Godramstein": null,
- "Gokels": null,
- "Goldbeck(Osterburg)": null,
- "Goldberg(Württ)": null,
- "Goldenstedt(Oldb)": null,
- "Goldhausen": null,
- "Goldshöfe": null,
- "Gollmitz(Niederlausitz)": null,
- "Golm": null,
- "Golzow(Eberswalde)": null,
- "Golzow(Oderbruch)": null,
- "Golßen(Niederlausitz)": null,
- "Gomadingen": null,
- "Gommern": null,
- "Gondelsheim Schloßstadion": null,
- "Gondelsheim(Baden)": null,
- "Gorgast": null,
- "Gosberg": null,
- "Goslar": null,
- "Gotha": null,
- "Gotha Ost": null,
- "Gottenheim": null,
- "Gotteszell": null,
- "Gottmadingen": null,
- "Goßdorf-Kohlmühle": null,
- "Goßfelden": null,
- "Goßmannsdorf": null,
- "Graal-Müritz": null,
- "Graal-Müritz Koppelweg": null,
- "Graben(Lechfeld)Gewerbepark": null,
- "Graben-Neudorf": null,
- "Graben-Neudorf Nord": null,
- "Grabow(Meckl)": null,
- "Grafenaschau": null,
- "Grafenau": null,
- "Grafenwiesen": null,
- "Grafing Bahnhof": null,
- "Grafing Stadt": null,
- "Grafling-Arzting": null,
- "Grafrath": null,
- "Grambow": null,
- "Gransee": null,
- "Grauschwitz Flocke": null,
- "Grebenstein": null,
- "Greifswald": null,
- "Greifswald Süd": null,
- "Greiz": null,
- "Greiz-Dölau": null,
- "Grenzach": null,
- "Greppin": null,
- "Greußen": null,
- "Greven": null,
- "Grevenbroich": null,
- "Grevesmühlen": null,
- "Grieben(Meckl)": null,
- "Griebo": null,
- "Griefstedt": null,
- "Griefstedt": null,
- "Griesen(Oberbay)": null,
- "Grießen(Baden)": null,
- "Grimma ob Bf": null,
- "Grimmen": null,
- "Grimmenthal": null,
- "Grobau": null,
- "Grombach": null,
- "Gronau(Westf)": null,
- "Gronsdorf": null,
- "Groß Ammensleben": null,
- "Groß Brütz": null,
- "Groß Düngen": null,
- "Groß Gerau": null,
- "Groß Gerau-Dornberg": null,
- "Groß Gerau-Dornheim": null,
- "Groß Karben": null,
- "Groß Kiesow": null,
- "Groß Kreutz": null,
- "Groß Köris": null,
- "Groß Laasch": null,
- "Groß Lüsewitz": null,
- "Groß Pankow": null,
- "Groß Quassow": null,
- "Groß Rohrheim": null,
- "Groß Schwaß": null,
- "Groß Schönebeck": null,
- "Groß-Umstadt Klein-Umstadt": null,
- "Groß-Umstadt Mitte": null,
- "Groß-Umstadt Wiebelsbach": null,
- "Großarmschlag": null,
- "Großauheim(Kr Hanau)": null,
- "Großbeeren": null,
- "Großbothen": null,
- "Großburgwedel": null,
- "Großdeuben": null,
- "Großen Buseck": null,
- "Großen Linden": null,
- "Großenaspe": null,
- "Großenbrode": null,
- "Großengottern": null,
- "Großenhain Cottb Bf": null,
- "Großenkneten": null,
- "Großenlüder": null,
- "Großfurra": null,
- "Großgeschaidt": null,
- "Großharthau": null,
- "Großhelfendorf": null,
- "Großheringen": null,
- "Großhesselohe Isartalbf": null,
- "Großkarolinenfeld": null,
- "Großkorbetha": null,
- "Großkrotzenburg": null,
- "Großkugel": null,
- "Großlehna": null,
- "Großneuhausen": null,
- "Großpösna": null,
- "Großrudestedt": null,
- "Großräschen": null,
- "Großröhrsdorf": null,
- "Großschwabhausen": null,
- "Großschönau(Sachs)": null,
- "Großsteinberg": null,
- "Großwalbur": null,
- "Großwudicke": null,
- "Grub am Forst": null,
- "Grub(Oberbay)": null,
- "Grub(Oberpf)": null,
- "Gruiten": null,
- "Gruiten": null,
- "Grunbach": null,
- "Grunow(Niederlausitz)": null,
- "Gräfelfing": null,
- "Gräfenberg": null,
- "Gräfendorf": null,
- "Gräfenhainichen": null,
- "Gräfenroda": null,
- "Gräfentonna": null,
- "Gräveneck": null,
- "Grävenwiesbach": null,
- "Gröbenzell": null,
- "Gröbers": null,
- "Gröditz(Riesa)": null,
- "Grötzingen": null,
- "Grötzingen Krappmühlenweg": null,
- "Grötzingen Oberausstraße": null,
- "Grüna(Sachs)Hp": null,
- "Grünbach(Vogtl)": null,
- "Grünberg(Oberhess)": null,
- "Grünebach Ort": null,
- "Grünebacherhütte": null,
- "Grüneberg": null,
- "Grünhainichen-Borstendorf": null,
- "Grünsfeld": null,
- "Grünstadt": null,
- "Grünstadt Nord": null,
- "Grüntal-Wittlensweiler": null,
- "Gstadt(Wanderbahn)": null,
- "Guben": null,
- "Gummersbach": null,
- "Gumpenried-Asbach": null,
- "Gundelfingen(Bay)": null,
- "Gundelfingen(Breisgau)": null,
- "Gundelsdorf": null,
- "Gundelshausen": null,
- "Gundelsheim(Neckar)": null,
- "Gundersheim(Rheinhess)": null,
- "Guntersblum": null,
- "Gunzenhausen": null,
- "Gussenstadt": null,
- "Gustorf": null,
- "Gutach Freilichtmuseum": null,
- "Gutach(Breisgau)": null,
- "Gutenfürst": null,
- "Guthmannshausen": null,
- "Guxhagen": null,
- "Gärtringen": null,
- "Gäufelden": null,
- "Göbelnrod": null,
- "Göhrde": null,
- "Göhren(Rügen)": null,
- "Göllheim-Dreisen": null,
- "Gölshausen": null,
- "Gölshausen Industriegebiet": null,
- "Göppingen": null,
- "Görden": null,
- "Görlitz": null,
- "Görlitz-Rauschwalde": null,
- "Görlitz-Weinhübel": null,
- "Görsbach": null,
- "Görschnitz": null,
- "Göttingen": null,
- "Götz": null,
- "Gößnitz": null,
- "Gültstein": null,
- "Gündlkofen": null,
- "Güntersberge": null,
- "Günzach": null,
- "Günzburg": null,
- "Güsen(b Genthin)": null,
- "Güsten": null,
- "Güstrow": null,
- "Güterglück": null,
- "Güterglück": null,
- "Gütersloh Hbf": null,
- "Haan": null,
- "Haar": null,
- "Haarhausen": null,
- "Hachenburg": null,
- "Hadamar": null,
- "Hademarschen": null,
- "Hadmersleben": null,
- "Haffkrug": null,
- "Hagebök": null,
- "Hagelstadt": null,
- "Hagen Hbf": null,
- "Hagen(Han)": null,
- "Hagen-Heubing": null,
- "Hagen-Oberhagen": null,
- "Hagen-Vorhalle": null,
- "Hagen-Wehringhausen": null,
- "Hagen-Westerbauer": null,
- "Hagenbach": null,
- "Hagenbüchach": null,
- "Hagenbüchach": null,
- "Hagenow Land": null,
- "Hagenow Stadt": null,
- "Hagenwerder": null,
- "Haidenaab-Göppmannsbühl": null,
- "Haidkapelle": null,
- "Haiger": null,
- "Haiger Obertor": null,
- "Haigerloch": null,
- "Haigerloch": null,
- "Hailer-Meerholz": null,
- "Hainburg Hainstadt": null,
- "Hainewalde": null,
- "Hainichen": null,
- "Hainstadt(Baden)": null,
- "Haitz-Höchst": null,
- "Halbe": null,
- "Halberstadt": null,
- "Halberstadt Oststr": null,
- "Halberstadt-Spiegelsberge": null,
- "Halbmeil": null,
- "Haldensleben": null,
- "Haldern(Rheinl)": null,
- "Halen": null,
- "Halfing": null,
- "Hallbergmoos": null,
- "Halle Dessauer Brücke": null,
- "Halle Messe": null,
- "Halle Rosengarten": null,
- "Halle Steintorbrücke": null,
- "Halle Südstadt": null,
- "Halle Wohnstadt Nord": null,
- "Halle Zoo": null,
- "Halle Zscherbener Straße": null,
- "Halle(Saale)Hbf": null,
- "Halle(Saale)Hbf Gl. 13a": null,
- "Halle(Saale)Hbf Gl. 13a": null,
- "Halle(W) Gerry-Weber-Stadion": null,
- "Halle(Westf)": null,
- "Halle-Ammendorf": null,
- "Halle-Ammendorf": null,
- "Halle-Neustadt": null,
- "Halle-Nietleben": null,
- "Halle-Silberhöhe": null,
- "Halle-Trotha": null,
- "Halle-Trotha": null,
- "Hallstadt(b Bamberg)": null,
- "Halstenbek": null,
- "Haltern am See": null,
- "Haltingen": null,
- "Hamburg Airport": null,
- "Hamburg Alte Wöhr": null,
- "Hamburg Berliner Tor": null,
- "Hamburg Berliner Tor": null,
- "Hamburg Berliner Tor": null,
- "Hamburg Billwerder-Moorfleet": null,
- "Hamburg Burgwedel": null,
- "Hamburg Dammtor": null,
- "Hamburg Dammtor": null,
- "Hamburg Diebsteich": null,
- "Hamburg Elbgaustraße": null,
- "Hamburg Friedrichsberg": null,
- "Hamburg Hasselbrook": null,
- "Hamburg Hasselbrook": null,
- "Hamburg Hbf": null,
- "Hamburg Hbf (S-Bahn)": null,
- "Hamburg Hochkamp": null,
- "Hamburg Hoheneichen": null,
- "Hamburg Holstenstraße": null,
- "Hamburg Jungfernstieg": null,
- "Hamburg Klein Flottbek": null,
- "Hamburg Kornweg(Klein Borstel)": null,
- "Hamburg Königstraße": null,
- "Hamburg Landungsbrücken": null,
- "Hamburg Landungsbrücken": null,
- "Hamburg Landwehr": null,
- "Hamburg Mittlerer Landweg": null,
- "Hamburg Neuwiedenthal": null,
- "Hamburg Reeperbahn": null,
- "Hamburg Rübenkamp": null,
- "Hamburg Stadthausbrücke": null,
- "Hamburg Wandsbeker Chaussee": null,
- "Hamburg-Allermöhe": null,
- "Hamburg-Altona": null,
- "Hamburg-Altona(S)": null,
- "Hamburg-Bahrenfeld": null,
- "Hamburg-Barmbek": null,
- "Hamburg-Bergedorf": null,
- "Hamburg-Bergedorf": null,
- "Hamburg-Blankenese": null,
- "Hamburg-Eidelstedt": null,
- "Hamburg-Eidelstedt": null,
- "Hamburg-Eidelstedt": null,
- "Hamburg-Eidelstedt Zentrum": null,
- "Hamburg-Fischbek": null,
- "Hamburg-Hammerbrook": null,
- "Hamburg-Harburg": null,
- "Hamburg-Harburg Rathaus": null,
- "Hamburg-Harburg(S)": null,
- "Hamburg-Heimfeld": null,
- "Hamburg-Hörgensweg": null,
- "Hamburg-Iserbrook": null,
- "Hamburg-Langenfelde": null,
- "Hamburg-Nettelnburg": null,
- "Hamburg-Neugraben": null,
- "Hamburg-Neugraben": null,
- "Hamburg-Ohlsdorf": null,
- "Hamburg-Othmarschen": null,
- "Hamburg-Poppenbüttel": null,
- "Hamburg-Rahlstedt": null,
- "Hamburg-Rissen": null,
- "Hamburg-Rissen": null,
- "Hamburg-Rothenburgsort": null,
- "Hamburg-Rothenburgsort": null,
- "Hamburg-Schnelsen": null,
- "Hamburg-Stellingen": null,
- "Hamburg-Stellingen": null,
- "Hamburg-Sternschanze": null,
- "Hamburg-Sülldorf": null,
- "Hamburg-Tiefstack": null,
- "Hamburg-Tonndorf": null,
- "Hamburg-Veddel": null,
- "Hamburg-Wandsbek": null,
- "Hamburg-Wellingsbüttel": null,
- "Hamburg-Wilhelmsburg": null,
- "Hameln": null,
- "Hamm(Westf)": null,
- "Hammah": null,
- "Hammelburg": null,
- "Hammelburg Ost": null,
- "Hammelspring": null,
- "Hammerau": null,
- "Hammerstein": null,
- "Hammerunterwiesenthal": null,
- "Hamminkeln": null,
- "Hanau Hbf": null,
- "Hanau Hbf": null,
- "Hanau Hbf": null,
- "Hanau Klein-Auheim": null,
- "Hanau Nord": null,
- "Hanau West": null,
- "Hanau-Wilhelmsbad": null,
- "Handeloh": null,
- "Hanfertal": null,
- "Hangelar Mitte": null,
- "Hangelsberg": null,
- "Hann Münden": null,
- "Hannover Anderten-Misburg": null,
- "Hannover Bismarckstr.": null,
- "Hannover Flughafen": null,
- "Hannover Hbf": null,
- "Hannover Karl-Wiechert-Allee": null,
- "Hannover Messe/Laatzen": null,
- "Hannover-Bornum": null,
- "Hannover-Kleefeld": null,
- "Hannover-Ledeburg": null,
- "Hannover-Leinhausen": null,
- "Hannover-Linden/Fischerhof": null,
- "Hannover-Nordstadt": null,
- "Hannover-Vinnhorst": null,
- "Happurg": null,
- "Harblek": null,
- "Harburg(Schwab)": null,
- "Hardegsen": null,
- "Hardhof": null,
- "Haren(Ems)": null,
- "Harra": null,
- "Harra Nord": null,
- "Harras(Thür)": null,
- "Harsdorf": null,
- "Harsefeld": null,
- "Harsum": null,
- "Hartenstein": null,
- "Hartershofen": null,
- "Harthaus": null,
- "Hartmannmühle": null,
- "Hartmannshof": null,
- "Harxheim-Zell": null,
- "Harzgerode": null,
- "Hasbergen": null,
- "Haselbrunn": null,
- "Haslach": null,
- "Hasloch(Main)": null,
- "Hasloh": null,
- "Haslohfurth": null,
- "Haspelmoor": null,
- "Hassel(Saar)": null,
- "Hasselborn": null,
- "Hasselfelde": null,
- "Haste": null,
- "Hattenheim": null,
- "Hattersheim(Main)": null,
- "Hattert": null,
- "Hattingen(R) Mitte": null,
- "Hattingen(Ruhr)": null,
- "Hattorf": null,
- "Hatzenport": null,
- "Haubersbronn": null,
- "Haubersbronn Mitte": null,
- "Hauenstein Mitte": null,
- "Hauenstein(Pfalz)": null,
- "Haunetal-Neukirchen": null,
- "Haupeltshofen": null,
- "Hauptstuhl": null,
- "Hausach": null,
- "Hausen (b Düren)": null,
- "Hausen i Tal": null,
- "Hausen(Eichsfeld)": null,
- "Hausen(Schwab)": null,
- "Hausen(Taunus)": null,
- "Hausen-Raitbach": null,
- "Hausen-Starzeln": null,
- "Hausham": null,
- "Havixbeck": null,
- "Haßfurt": null,
- "Haßloch(Pfalz)": null,
- "Haßmersheim": null,
- "Hebertsfelden": null,
- "Hebertshausen": null,
- "Hechingen": null,
- "Hechingen Landesbahn": null,
- "Hechthausen": null,
- "Heddesheim/Hirschberg": null,
- "Hedemünden": null,
- "Hedersdorf": null,
- "Hedersleben-Wedderstedt": null,
- "Heessen": null,
- "Heggen": null,
- "Hegne": null,
- "Heide(Holst)": null,
- "Heidelberg Hbf": null,
- "Heidelberg Orthopädie": null,
- "Heidelberg-Altstadt": null,
- "Heidelberg-Kirchheim/Rohrbach": null,
- "Heidelberg-Pfaffengrund/Wieblingen": null,
- "Heidelberg-Schlierbach/Ziegelhausen": null,
- "Heidelberg-Weststadt/Südstadt": null,
- "Heidelsheim": null,
- "Heidelsheim Nord": null,
- "Heidenau": null,
- "Heidenau Süd": null,
- "Heidenau-Großsedlitz": null,
- "Heidenheim": null,
- "Heidenheim Voithwerk": null,
- "Heidenheim-Mergelstetten": null,
- "Heidenheim-Schnaitheim": null,
- "Heidesheim(Rheinhess)": null,
- "Heidkrug": null,
- "Heigenbrücken": null,
- "Heilbad Heiligenstadt": null,
- "Heilbr.-Böckingen Berufsschulzentrum": null,
- "Heilbronn Finanzamt": null,
- "Heilbronn Friedensplatz": null,
- "Heilbronn Hans-Rießer-Straße": null,
- "Heilbronn Harmonie": null,
- "Heilbronn Harmonie/Hafenmarktpassage": null,
- "Heilbronn Harmonie/Kunsthalle": null,
- "Heilbronn Hauptbahnhof/Willy-Brandt-Pl.": null,
- "Heilbronn Hbf": null,
- "Heilbronn Industrieplatz": null,
- "Heilbronn Kaufland": null,
- "Heilbronn Neckar-Turm/K.-S.-Pl": null,
- "Heilbronn Pfühlpark": null,
- "Heilbronn Rathaus": null,
- "Heilbronn Sülmertor": null,
- "Heilbronn Technisches Schulzentrum": null,
- "Heilbronn Theater": null,
- "Heilbronn Trappensee": null,
- "Heiligendamm": null,
- "Heiligengrabe": null,
- "Heiligenstatt(Obb)": null,
- "Heiligenstein(Pfalz)": null,
- "Heilsbronn": null,
- "Heimbach (Eifel)": null,
- "Heimbach(Nahe)": null,
- "Heimbach(Nahe)Ort": null,
- "Heimenkirch": null,
- "Heimersheim": null,
- "Heimstetten": null,
- "Heinebach": null,
- "Heinsberg Kreishaus": null,
- "Heinsberg(Rheinl)": null,
- "Heinsberg-Dremmen": null,
- "Heinsberg-Horst": null,
- "Heinsberg-Oberbruch": null,
- "Heinsberg-Porselen": null,
- "Heinsberg-Randerath": null,
- "Heinschenwalde": null,
- "Heinzenhausen": null,
- "Heitersheim": null,
- "Heldrungen": null,
- "Helenesee": null,
- "Helmbrechts": null,
- "Helmsdorf(Pirna)": null,
- "Helmsheim": null,
- "Helmstadt(Baden)": null,
- "Helmstedt": null,
- "Helpup": null,
- "Hemmerde": null,
- "Hemmersdorf(Saar)": null,
- "Hemmingen": null,
- "Hemmoor": null,
- "Hemsbach": null,
- "Henfenfeld": null,
- "Hennef im Siegbogen": null,
- "Hennef(Sieg)": null,
- "Hennen": null,
- "Hennersdorf(Sachs)": null,
- "Hennigsdorf(b Berlin)": null,
- "Henstedt-Ulzburg": null,
- "Heppenheim(Bergstr)": null,
- "Herbertingen": null,
- "Herbertingen Ort": null,
- "Herbertshofen": null,
- "Herblingen": null,
- "Herbolzheim(Breisg)": null,
- "Herbolzheim(Jagst)": null,
- "Herborn(Dillkr)": null,
- "Herbrechtingen": null,
- "Herchen": null,
- "Herdecke": null,
- "Herdorf": null,
- "Herford": null,
- "Hergatz": null,
- "Hergershausen": null,
- "Heringen(Helme)": null,
- "Heringsdorf Neuhof": null,
- "Herlasgrün": null,
- "Herleshausen Hp": null,
- "Hermaringen": null,
- "Hermentingen": null,
- "Hermsdorf(Dresden)": null,
- "Hermsdorf-Klosterlausnitz": null,
- "Herne": null,
- "Herne-Börnig": null,
- "Heroldsberg": null,
- "Heroldsberg Nord": null,
- "Herrath": null,
- "Herrenberg": null,
- "Herrenberg": null,
- "Herrenberg Zwerchweg": null,
- "Herrensee": null,
- "Herrlingen": null,
- "Herrlishöfen": null,
- "Herrnburg": null,
- "Herrsching": null,
- "Hersbruck(l Pegnitz)": null,
- "Hersbruck(r Pegnitz)": null,
- "Herten(Baden)": null,
- "Hervest-Dorsten": null,
- "Herxheim am Berg": null,
- "Herzberg Schloß": null,
- "Herzberg(Elster)": null,
- "Herzberg(Harz)": null,
- "Herzberg(Mark)": null,
- "Herzebrock": null,
- "Herzhorn": null,
- "Herzogenrath": null,
- "Herzogenrath-Alt-Merkstein": null,
- "Herzogenrath-August-Schmidt-Platz": null,
- "Hesedorf": null,
- "Heselbach": null,
- "Hesepe": null,
- "Hesseln": null,
- "Hesseneck Kailbach": null,
- "Hesseneck Schöllenbach": null,
- "Hessisch Oldendorf": null,
- "Hetschburg": null,
- "Hettenhausen": null,
- "Hettingen(Hohenz)": null,
- "Hettstedt": null,
- "Hetzdorf(Flöhatal)": null,
- "Hetzerath": null,
- "Heudeber-Danstedt": null,
- "Heufeld": null,
- "Heufeldmühle": null,
- "Heusenstamm": null,
- "Hiddenhausen-Schweicheln": null,
- "Hilchenbach": null,
- "Hildburghausen": null,
- "Hilden": null,
- "Hilden Süd": null,
- "Hildesheim Hbf": null,
- "Hildesheim Ost": null,
- "Hillnhütten": null,
- "Hilpertsau": null,
- "Hilpoltstein": null,
- "Hilter": null,
- "Himmelpforten": null,
- "Himmelreich": null,
- "Himmelstadt": null,
- "Hinrichssegen": null,
- "Hinterweidenthal": null,
- "Hinterweidenthal Ort": null,
- "Hinterweidenthal Ost": null,
- "Hinterzarten": null,
- "Hirsau": null,
- "Hirschaid": null,
- "Hirschfelde": null,
- "Hirschfelden": null,
- "Hirschhorn(Neckar)": null,
- "Hirschhorn(Pfalz)": null,
- "Hittfeld": null,
- "Hitzacker": null,
- "Hochdahl": null,
- "Hochdahl-Millrath": null,
- "Hochdorf(b Horb)": null,
- "Hochhausen(Tauber)": null,
- "Hochheim(Main)": null,
- "Hochneukirch": null,
- "Hochspeyer": null,
- "Hochstadt-Marktzeuln": null,
- "Hochstetten": null,
- "Hochstetten Grenzstraße": null,
- "Hochstetten(Nahe)": null,
- "Hochstätten(Pfalz)": null,
- "Hochwang": null,
- "Hockenheim": null,
- "Hockeroda": null,
- "Hodenhagen": null,
- "Hof Hbf": null,
- "Hof(Münstertal)": null,
- "Hof-Neuhof": null,
- "Hofeld": null,
- "Hofen(b Aalen)": null,
- "Hoffenheim": null,
- "Hoffnungsthal": null,
- "Hofgeismar": null,
- "Hofgeismar-Hümme": null,
- "Hofheim (Ried)": null,
- "Hofheim(Taunus)": null,
- "Hohegrete": null,
- "Hohen Neuendorf West": null,
- "Hohen Neuendorf(b Berlin)": null,
- "Hohenbrunn": null,
- "Hohendorf": null,
- "Hohenebra Ort": null,
- "Hoheneggelsen": null,
- "Hohenfichte": null,
- "Hohenleipisch": null,
- "Hohenleuben": null,
- "Hohenlimburg": null,
- "Hohenpeißenberg": null,
- "Hohenroda": null,
- "Hohenschäftlarn": null,
- "Hohenstadt(Mittelfr)": null,
- "Hohenstein-Ernstthal": null,
- "Hohensülzen": null,
- "Hohenthurm": null,
- "Hohenwarth": null,
- "Hohenwarth Campingplatz": null,
- "Hohenwestedt": null,
- "Hohenwulsch": null,
- "Hohndorf Mitte": null,
- "Holdorf(Meckl)": null,
- "Holdorf(Oldb)": null,
- "Holm-Seppensen": null,
- "Holstentherme": null,
- "Holtensen/Linderte": null,
- "Holthusen": null,
- "Holzdorf(Elster)": null,
- "Holzdorf(b Weimar)": null,
- "Holzgerlingen Bf": null,
- "Holzgerlingen Buch": null,
- "Holzgerlingen Nord": null,
- "Holzhau": null,
- "Holzhau Skilift": null,
- "Holzhausen(Kr Siegen)": null,
- "Holzhausen-Heddinghausen": null,
- "Holzheim(b Neuss)": null,
- "Holzkirchen": null,
- "Holzminden": null,
- "Holzwickede": null,
- "Homburg(Saar)Hbf": null,
- "Honrath": null,
- "Hopfgarten(Sachs)": null,
- "Hopfgarten(Weimar)": null,
- "Hoppecke": null,
- "Hoppegarten(Mark)": null,
- "Hoppegarten(Mark)": null,
- "Hoppingen": null,
- "Hoppstädten(Nahe)": null,
- "Horb": null,
- "Horb-Heiligenfeld": null,
- "Horka": null,
- "Horn-Bad Meinberg": null,
- "Hornberg(Schwarzw)": null,
- "Horneburg": null,
- "Hornstorf": null,
- "Horrem": null,
- "Horst(Holst)": null,
- "Hosena": null,
- "Hoyerswerda": null,
- "Hoykenkamp": null,
- "Hubacker": null,
- "Hubertushöhe": null,
- "Huchem-Stammeln": null,
- "Huckstorf": null,
- "Hude": null,
- "Hufschlag": null,
- "Huglfing": null,
- "Hugstetten": null,
- "Hulb": null,
- "Hummelberg": null,
- "Hundsgrün": null,
- "Hundstadt": null,
- "Hungen": null,
- "Huntlosen": null,
- "Husby": null,
- "Husum": null,
- "Huttenheim": null,
- "Huzenbach": null,
- "Hähnichen": null,
- "Hähnlein-Alsbach": null,
- "Hämelerwald": null,
- "Hämerten": null,
- "Häuserhof": null,
- "Höchst Hetschbach": null,
- "Höchst Mümling-Grumbach": null,
- "Höchst(Odenw)": null,
- "Höchstädt(Donau)": null,
- "Höfen(Enz) Bf": null,
- "Höfen(Enz) Nord": null,
- "Höfingen": null,
- "Höhenkirchen-Siegertsbrunn": null,
- "Höhmühlbach": null,
- "Höllenthal": null,
- "Höllriegelskreuth": null,
- "Höpfling": null,
- "Hörden": null,
- "Hörlkofen": null,
- "Hörpolding": null,
- "Hörschel Hp": null,
- "Hörselgau": null,
- "Hörstel": null,
- "Hörstmar(Lippe)": null,
- "Hösbach": null,
- "Hösel": null,
- "Hövelhof": null,
- "Hövelriege": null,
- "Höxter Rathaus": null,
- "Hückelhoven-Baal": null,
- "Hüffenhardt": null,
- "Hüfingen Mitte": null,
- "Hünfeld": null,
- "Hürth-Kalscheuren": null,
- "Hütten": null,
- "Hüttenbusch": null,
- "Hüttengrund": null,
- "Hüttingen": null,
- "Ibach": null,
- "Ibbenbüren": null,
- "Ibbenbüren-Esch": null,
- "Ibbenbüren-Laggenbeck": null,
- "Ichenhausen": null,
- "Icking": null,
- "Idar-Oberstein": null,
- "Idstein(Taunus)": null,
- "Iffeldorf": null,
- "Igel": null,
- "Igensdorf": null,
- "Igersheim": null,
- "Ihringen": null,
- "Ilberstedt": null,
- "Ilfeld": null,
- "Ilfeld Bad": null,
- "Ilfeld Neanderklinik": null,
- "Ilfeld Schreiberwiese": null,
- "Illertissen": null,
- "Illesheim": null,
- "Illingen(Saar)": null,
- "Illingen(Württ)": null,
- "Ilmenau": null,
- "Ilmenau Bad": null,
- "Ilmenau Pörlitzer Höhe": null,
- "Ilmenau-Roda": null,
- "Ilsenburg": null,
- "Immelborn": null,
- "Immendingen": null,
- "Immendingen Mitte": null,
- "Immendingen Zimmern": null,
- "Immenhausen": null,
- "Immenreuth": null,
- "Immensen-Arpke": null,
- "Immenstadt": null,
- "Imsweiler": null,
- "Ingelbach": null,
- "Ingelheim": null,
- "Ingolstadt Hbf": null,
- "Ingolstadt Nord": null,
- "Inningen": null,
- "Inselstadt Malchow": null,
- "Insheim": null,
- "Iphofen": null,
- "Ipsheim": null,
- "Irfersgrün": null,
- "Irrenlohe": null,
- "Iserlohn": null,
- "Iserlohnerheide": null,
- "Isernhagen": null,
- "Ismaning": null,
- "Ispringen": null,
- "Isselhorst-Avenwedde": null,
- "Istein": null,
- "Ittersbach Bahnhof": null,
- "Ittersbach Rathaus": null,
- "Ittling": null,
- "Ittlingen": null,
- "Itzehoe": null,
- "Itzelberg": null,
- "Jabel(Meckl)": null,
- "Jacobsdorf(Mark)": null,
- "Jagdschloß": null,
- "Jagstzell": null,
- "Jahnsdorf(Erzgeb)": null,
- "Jakobwüllesheim": null,
- "Jarrenwisch": null,
- "Jasnitz": null,
- "Jatznick": null,
- "Jeber-Bergfrieden": null,
- "Jechtingen": null,
- "Jeeser": null,
- "Jena Paradies": null,
- "Jena Saalbf": null,
- "Jena West": null,
- "Jena-Göschwitz": null,
- "Jena-Zwätzen": null,
- "Jesewitz(Leipzig)": null,
- "Jessen(Elster)": null,
- "Jettenbach": null,
- "Jettingen": null,
- "Jever": null,
- "Jeßnitz(Anh)": null,
- "Joachimsthal": null,
- "Joachimsthal Kaiserbahnhof": null,
- "Jocketa": null,
- "Jockgrim Bf": null,
- "Johanngeorgenstadt": null,
- "Jossa": null,
- "Julbach": null,
- "Jungingen(Hohenz)": null,
- "Jungnau": null,
- "Jägerhaus": null,
- "Jägersfreude": null,
- "Jänschwalde": null,
- "Jänschwalde Ost": null,
- "Jävenitz": null,
- "Jöhlingen": null,
- "Jöhlingen": null,
- "Jöhlingen West": null,
- "Jößnitz": null,
- "Jübek": null,
- "Jüchen": null,
- "Jülich": null,
- "Jülich Forschungszentrum": null,
- "Jülich-Broich": null,
- "Jülich-Nord": null,
- "Jülich-Selgersdorf": null,
- "Jünkerath": null,
- "Jüterbog": null,
- "Kaarst IKEA": null,
- "Kaarst Mitte/Holzbüttgen": null,
- "Kaarster Bahnhof": null,
- "Kaarster See": null,
- "Kablow": null,
- "Kahl Kopp/Heide": null,
- "Kahl(Main)": null,
- "Kahl(Main)": null,
- "Kahla(Thür)": null,
- "Kainzenbad": null,
- "Kaisersesch": null,
- "Kaiserslautern Hbf": null,
- "Kaiserslautern Pfaffwerk": null,
- "Kaiserslautern West": null,
- "Kalchreuth": null,
- "Kaldenkirchen": null,
- "Kalenborn(Westerw)": null,
- "Kall": null,
- "Kalsow": null,
- "Kalteneck": null,
- "Kaltenkirchen Süd": null,
- "Kaltenkirchen(Holst)": null,
- "Kalthof(Kr Iserlohn)": null,
- "Kamen": null,
- "Kamen-Methler": null,
- "Kamenz(Sachs)": null,
- "Kamp-Bornhofen": null,
- "Kandel": null,
- "Kandern": null,
- "Kanzem": null,
- "Kapellen-Drusweiler": null,
- "Kapellen-Wevelinghoven": null,
- "Kapen Biosphärenreservat": null,
- "Kappelrodeck": null,
- "Kappelrodeck Ost": null,
- "Kapsweyer": null,
- "Karlsburg": null,
- "Karlsdorf": null,
- "Karlshagen": null,
- "Karlsruhe Albtalbf": null,
- "Karlsruhe Bahnhofsvorplatz": null,
- "Karlsruhe Durlacher Tor": null,
- "Karlsruhe Entenfang": null,
- "Karlsruhe Hbf": null,
- "Karlsruhe Marktplatz (Kaiserstraße)": null,
- "Karlsruhe Mühlburger Tor": null,
- "Karlsruhe West": null,
- "Karlsruhe West": null,
- "Karlsruhe-Durlach": null,
- "Karlsruhe-Durlach": null,
- "Karlsruhe-Hagsfeld": null,
- "Karlsruhe-Kniel. Rheinbergstr.": null,
- "Karlsruhe-Knielingen": null,
- "Karlsruhe-Mühlburg": null,
- "Karlsruhe-Neureut Kirchfeld": null,
- "Karlstadt(Main)": null,
- "Karpfham": null,
- "Karsdorf": null,
- "Karstädt": null,
- "Karthaus": null,
- "Kasbach": null,
- "Kasbach Brauerei Steffens": null,
- "Kassel Hbf": null,
- "Kassel Hbf (tief)": null,
- "Kassel-Harleshausen": null,
- "Kassel-Jungfernkopf": null,
- "Kassel-Kirchditmold": null,
- "Kassel-Oberzwehren": null,
- "Kassel-Wilhelmshöhe": null,
- "Kastl(Oberbay)": null,
- "Katharinenheerd": null,
- "Kating": null,
- "Katlenburg": null,
- "Kattenes": null,
- "Kattenvenne": null,
- "Katzenfurt": null,
- "Katzhütte": null,
- "Katzwang": null,
- "Katzweiler": null,
- "Kaub": null,
- "Kaufbeuren": null,
- "Kaufering": null,
- "Kaulsdorf(Saale)": null,
- "Kavelstorf(Kr Rostock)": null,
- "Kehl": null,
- "Kehlen": null,
- "Keitum": null,
- "Kelkheim": null,
- "Kelkheim-Hornau": null,
- "Kelkheim-Münster": null,
- "Kellmünz": null,
- "Kelsterbach": null,
- "Kemnath-Neustadt": null,
- "Kempen(Niederrhein)": null,
- "Kempten(Allgäu)Hbf": null,
- "Kempten(Allgäu)Ost": null,
- "Kemtau": null,
- "Kennelgarten": null,
- "Kenz": null,
- "Kenzingen": null,
- "Kerkerbach": null,
- "Kerkwitz": null,
- "Kersbach": null,
- "Kestert": null,
- "Kettwig": null,
- "Kettwig Stausee": null,
- "Kevelaer": null,
- "Kiebingen": null,
- "Kiebitzhöhe": null,
- "Kiefersfelden": null,
- "Kiel Hbf": null,
- "Kiel Schulen am Langsee": null,
- "Kiel-Elmschenhagen": null,
- "Kiel-Hassee CITTI-PARK": null,
- "Kiel-Russee": null,
- "Killer": null,
- "Kinding(Altmühltal)": null,
- "Kindsbach": null,
- "Kirch Göns": null,
- "Kirch-Jesar": null,
- "Kirchanschöring": null,
- "Kirchberg(Murr)": null,
- "Kirchdorf(Deister)": null,
- "Kirchehrenbach": null,
- "Kirchen": null,
- "Kirchenlaibach": null,
- "Kirchenlamitz Ost": null,
- "Kirchentellinsfurt": null,
- "Kirchhain(Bz Kassel)": null,
- "Kirchhammelwarden": null,
- "Kirchheim(Neckar)": null,
- "Kirchheim(Teck)": null,
- "Kirchheim(Teck)-Ötlingen": null,
- "Kirchheim(Teck)Süd": null,
- "Kirchheim(Unterfr)": null,
- "Kirchheim(Weinstr)": null,
- "Kirchheimbolanden": null,
- "Kirchhorsten": null,
- "Kirchhundem": null,
- "Kirchlengern": null,
- "Kirchmöser": null,
- "Kirchscheidungen": null,
- "Kirchseeon": null,
- "Kirchseeon": null,
- "Kirchweidach": null,
- "Kirchweyhe": null,
- "Kirchzarten": null,
- "Kirkel": null,
- "Kirn": null,
- "Kirnbach-Grün": null,
- "Kirnsulzbach": null,
- "Kirschbaumwasen": null,
- "Kissing": null,
- "Kitzingen": null,
- "Kißlegg": null,
- "Klaffenbach Hp": null,
- "Klais": null,
- "Klandorf": null,
- "Klanxbüll": null,
- "Klasdorf Glashütte": null,
- "Klecken": null,
- "Klein Bünzow": null,
- "Klein Gerau": null,
- "Klein Winternheim-Ober Olm": null,
- "Kleinberghofen": null,
- "Kleinenbroich": null,
- "Kleinensiel": null,
- "Kleinforst Rosensee": null,
- "Kleinfurra": null,
- "Kleingemünden": null,
- "Kleinheubach": null,
- "Kleinjena": null,
- "Kleinkems": null,
- "Kleinkötz": null,
- "Kleinostheim": null,
- "Kleinröhrsdorf": null,
- "Kleinschirma": null,
- "Kleinsteinbach": null,
- "Kleinwallstadt": null,
- "Kleve": null,
- "Klieken": null,
- "Klinge": null,
- "Klingenberg(Main)": null,
- "Klingenberg-Colmnitz": null,
- "Klingenbrunn": null,
- "Klingenthal": null,
- "Klinikum Bremen-Nord/Beckedorf": null,
- "Klitschmar": null,
- "Kloster Bronnbach": null,
- "Kloster Marienthal": null,
- "Kloster Oesede": null,
- "Klosterbuch": null,
- "Klosterfelde": null,
- "Klosterlechfeld": null,
- "Klostermansfeld": null,
- "Klosterreichenbach": null,
- "Klotten": null,
- "Kläden(Stendal)": null,
- "Knautnaundorf": null,
- "Knesebeck": null,
- "Knittlingen-Kleinvillars": null,
- "Knöringen-Essingen": null,
- "Kobern-Gondorf": null,
- "Koblenz Hbf": null,
- "Koblenz Stadtmitte": null,
- "Koblenz-Ehrenbreitstein": null,
- "Koblenz-Güls": null,
- "Koblenz-Lützel": null,
- "Koblenz-Moselweiß": null,
- "Kochel": null,
- "Kodersdorf": null,
- "Kohlscheid": null,
- "Kohlstetten": null,
- "Kolbermoor": null,
- "Kolkwitz": null,
- "Kolkwitz Süd": null,
- "Kollmarsreute": null,
- "Kollnau": null,
- "Konstanz": null,
- "Konstanz-Fürstenberg": null,
- "Konstanz-Petershausen": null,
- "Konstanz-Wollmatingen": null,
- "Konz": null,
- "Konz Mitte": null,
- "Konz Mitte": null,
- "Korbach": null,
- "Korbach Süd": null,
- "Kordel": null,
- "Kork": null,
- "Korntal": null,
- "Korntal Gymnasium": null,
- "Kornwestheim Pbf": null,
- "Korschenbroich": null,
- "Koserow": null,
- "Kothmaißling": null,
- "Kottenheim": null,
- "Kraftsdorf": null,
- "Kraftwerk Finkenheerd": null,
- "Kraghammer": null,
- "Kranichfeld": null,
- "Kratzeburg": null,
- "Krauthausen": null,
- "Kredenbach": null,
- "Krefeld Hbf": null,
- "Krefeld-Hohenbudberg Chempark": null,
- "Krefeld-Linn": null,
- "Krefeld-Linn": null,
- "Krefeld-Oppum": null,
- "Krefeld-Uerdingen": null,
- "Kreiensen": null,
- "Kreimbach-Kaulbach": null,
- "Kremmen": null,
- "Krempe": null,
- "Kremperheide": null,
- "Krensitz": null,
- "Kressbronn": null,
- "Kretscham-Rothensehma": null,
- "Kreuz Konz": null,
- "Kreuzau Bahnhof": null,
- "Kreuzau-Eifelstraße": null,
- "Kreuzberg(Ahr)": null,
- "Kreuzstraße": null,
- "Kreuztal": null,
- "Kreuztal-Littfeld": null,
- "Kriftel": null,
- "Krippen": null,
- "Kronach": null,
- "Kronberg Süd": null,
- "Kronberg(Taunus)": null,
- "Kronshagen": null,
- "Kronskamp": null,
- "Kronweiler": null,
- "Kruft": null,
- "Krumbach(Schw)Schule": null,
- "Krumbach(Schwab)": null,
- "Krumhermsdorf": null,
- "Krumpa": null,
- "Krupunder": null,
- "Krölpa-Ranis": null,
- "Kröpelin": null,
- "Kubschütz": null,
- "Kuchen": null,
- "Kulmbach": null,
- "Kummerow(Stralsund)": null,
- "Kummersdorf(Storkow)": null,
- "Kunersdorf": null,
- "Kupfermühle": null,
- "Kuppenheim": null,
- "Kurort Altenberg(Erzgebirge)": null,
- "Kurort Jonsdorf": null,
- "Kurort Jonsdorf Hst": null,
- "Kurort Oberwiesenthal": null,
- "Kurort Oybin": null,
- "Kurort Oybin-Niederdorf": null,
- "Kurort Rathen": null,
- "Kusel": null,
- "Kutenholz": null,
- "Kutzenhausen": null,
- "Kyhna": null,
- "Kyllburg": null,
- "Kyritz": null,
- "Kälberau": null,
- "Kämmereiforst": null,
- "Köditz": null,
- "Köfering": null,
- "Kölleda": null,
- "Köln Airport-Businesspark": null,
- "Köln Frankfurter Straße": null,
- "Köln Geldernstr./Parkgürtel": null,
- "Köln Hansaring": null,
- "Köln Hbf": null,
- "Köln Messe/Deutz": null,
- "Köln Messe/Deutz Gl. 9-10": null,
- "Köln Messe/Deutz Gl.11-12": null,
- "Köln Steinstraße": null,
- "Köln Süd": null,
- "Köln Trimbornstr": null,
- "Köln Volkhovener Weg": null,
- "Köln West": null,
- "Köln-Blumenberg": null,
- "Köln-Buchforst": null,
- "Köln-Chorweiler": null,
- "Köln-Chorweiler Nord": null,
- "Köln-Dellbrück": null,
- "Köln-Ehrenfeld": null,
- "Köln-Ehrenfeld": null,
- "Köln-Holweide": null,
- "Köln-Longerich": null,
- "Köln-Mülheim": null,
- "Köln-Müngersdorf Technologiepark": null,
- "Köln-Nippes": null,
- "Köln-Nippes": null,
- "Köln-Stammheim": null,
- "Köln-Weiden West": null,
- "Köln-Worringen": null,
- "Köln/Bonn Flughafen": null,
- "Kölpinsee": null,
- "Köndringen": null,
- "Königs Wusterhausen": null,
- "Königsbach(Baden)": null,
- "Königsborn": null,
- "Königsbronn": null,
- "Königsbrück": null,
- "Königschaffhausen": null,
- "Königshofen(Baden)": null,
- "Königshofen(Kahl)": null,
- "Königslutter": null,
- "Königsstollen": null,
- "Königstein(Sächs Schw)": null,
- "Königstein(Taunus)": null,
- "Königswinter": null,
- "Königswinter Fähre": null,
- "Königswinter, Clem.-August-Str.": null,
- "Könitz(Thür)": null,
- "Könnern": null,
- "Köppern": null,
- "Körle": null,
- "Köthen": null,
- "Köttewitz": null,
- "Kötzschau": null,
- "Kövenig": null,
- "Kühnhausen": null,
- "Kühren": null,
- "Külte-Wetterburg": null,
- "Künsebeck": null,
- "Küntrop": null,
- "Küps": null,
- "Kürbitz": null,
- "Küstrin-Kietz": null,
- "Laaber": null,
- "Laage(Meckl)": null,
- "Laberweinting": null,
- "Ladenburg": null,
- "Lage(Lippe)": null,
- "Lagerlechfeld": null,
- "Lahr(Schwarzw)": null,
- "Laineck": null,
- "Lalendorf": null,
- "Lam": null,
- "Lambrecht(Pfalz)": null,
- "Lambsheim": null,
- "Lampertheim": null,
- "Lampertsmühle-Otterbach": null,
- "Lampertswalde": null,
- "Lancken": null,
- "Landau(Isar)": null,
- "Landau(Pfalz)Hbf": null,
- "Landau(Pfalz)Süd": null,
- "Landau(Pfalz)West": null,
- "Landsberg(L)Schule": null,
- "Landsberg(Lech)": null,
- "Landsberg(b. Halle/Saale)": null,
- "Landsberg(b. Halle/Saale) Süd": null,
- "Landshut(Bay)Hbf": null,
- "Landshut(Bay)Süd": null,
- "Landstuhl": null,
- "Landsweiler-Reden": null,
- "Lang Göns": null,
- "Langdorf": null,
- "Langebrück(Sachs)": null,
- "Langeln(Holst)": null,
- "Langelsheim": null,
- "Langen(Hess)": null,
- "Langen-Flugsicherung": null,
- "Langenargen": null,
- "Langenau(Württ)": null,
- "Langenbach(Oberbay)": null,
- "Langenbrand": null,
- "Langendorf": null,
- "Langeneichstädt": null,
- "Langenfeld(Rhld)": null,
- "Langenfeld(Rhld)-Berghausen": null,
- "Langenhagen Mitte": null,
- "Langenhagen Pferdemarkt": null,
- "Langenhagen-Kaltenweide": null,
- "Langenhahn": null,
- "Langenhorn(Schlesw)": null,
- "Langenlonsheim": null,
- "Langenmoor": null,
- "Langenorla Ost": null,
- "Langenorla West": null,
- "Langenprozelten": null,
- "Langenselbold": null,
- "Langenstein": null,
- "Langensteinbach Bahnhof": null,
- "Langenwang(Schwab)": null,
- "Langenweddingen": null,
- "Langenwolmsdorf": null,
- "Langenwolmsdorf Mitte": null,
- "Langenzenn": null,
- "Langerwehe": null,
- "Langhagen": null,
- "Langlau": null,
- "Langquaid(b Eggmühl)": null,
- "Langsdorf(Oberhess)": null,
- "Langwedel": null,
- "Langweid(Lech)": null,
- "Lathen": null,
- "Laubendorf": null,
- "Laubenheim(Nahe)": null,
- "Laucha(Unstrut)": null,
- "Lauchhammer": null,
- "Lauchheim": null,
- "Lauchringen": null,
- "Lauchringen West": null,
- "Lauda": null,
- "Laudenbach am Main": null,
- "Laudenbach(Bergstr)": null,
- "Laudenbach(Württ)": null,
- "Lauenbrück": null,
- "Lauenburg(Elbe)": null,
- "Lauenförde-Beverungen": null,
- "Lauenstein(Sachs)": null,
- "Lauf West": null,
- "Lauf(links Pegnitz)": null,
- "Lauf(rechts Pegnitz)": null,
- "Laufach": null,
- "Laufen(Oberbay)": null,
- "Laufenburg(Baden)": null,
- "Laufenburg(Baden)Ost": null,
- "Lauffen(Neckar)": null,
- "Lauingen": null,
- "Laupheim Stadt": null,
- "Laupheim West": null,
- "Laurenburg(Lahn)": null,
- "Lauscha(Thür)": null,
- "Lauta(Nl)": null,
- "Lautenbach(Baden)": null,
- "Lauter(Sachs)": null,
- "Lauterbach Mole": null,
- "Lauterbach(Hess)Nord": null,
- "Lauterbach(Rügen)": null,
- "Lauterbach-Steinbach": null,
- "Lauterecken-Grumbach": null,
- "Laußig(Düben)": null,
- "Laußnitz": null,
- "Lebach": null,
- "Lebach-Jabach": null,
- "Leer(Ostfriesl)": null,
- "Leese-Stolzenau": null,
- "Legden": null,
- "Legefeld": null,
- "Legelshurst": null,
- "Lehmen": null,
- "Lehndorf(Altenburg)": null,
- "Lehnheim": null,
- "Lehnitz": null,
- "Lehrte": null,
- "Leichlingen": null,
- "Leiferde(b Gifhorn)": null,
- "Leimstruth": null,
- "Leinefelde": null,
- "Leinfelden": null,
- "Leingarten": null,
- "Leingarten Mitte": null,
- "Leingarten Ost": null,
- "Leingarten West": null,
- "Leipheim": null,
- "Leipzig Allee-Center": null,
- "Leipzig Anger-Crottendorf": null,
- "Leipzig Bayerischer Bahnhof": null,
- "Leipzig Coppiplatz": null,
- "Leipzig Grünauer Allee": null,
- "Leipzig Hbf": null,
- "Leipzig Hbf (tief)": null,
- "Leipzig Karlsruher Str": null,
- "Leipzig MDR": null,
- "Leipzig Markt": null,
- "Leipzig Messe": null,
- "Leipzig Messe": null,
- "Leipzig Miltitzer Allee": null,
- "Leipzig Nord": null,
- "Leipzig Olbrichtstraße": null,
- "Leipzig Slevogtstraße": null,
- "Leipzig Völkerschlachtdenkmal": null,
- "Leipzig Werkstättenstraße": null,
- "Leipzig Wilhelm-Leuschner-Platz": null,
- "Leipzig-Connewitz": null,
- "Leipzig-Engelsdorf": null,
- "Leipzig-Engelsdorf": null,
- "Leipzig-Gohlis": null,
- "Leipzig-Heiterblick": null,
- "Leipzig-Holzhausen": null,
- "Leipzig-Knauthain": null,
- "Leipzig-Leutzsch": null,
- "Leipzig-Liebertwolkwitz": null,
- "Leipzig-Lindenau": null,
- "Leipzig-Lützschena": null,
- "Leipzig-Miltitz": null,
- "Leipzig-Möckern": null,
- "Leipzig-Mölkau": null,
- "Leipzig-Paunsdorf": null,
- "Leipzig-Paunsdorf": null,
- "Leipzig-Plagwitz": null,
- "Leipzig-Rückmarsdorf": null,
- "Leipzig-Sellerhausen": null,
- "Leipzig-Stötteritz": null,
- "Leipzig-Thekla": null,
- "Leipzig-Wahren": null,
- "Leipzig/Halle Flughafen": null,
- "Leisnig": null,
- "Leitstade": null,
- "Leißling": null,
- "Lembeck": null,
- "Lemförde": null,
- "Lemgo": null,
- "Lemgo-Lüttfeld": null,
- "Lemmie": null,
- "Lendringsen": null,
- "Lengede-Broistedt": null,
- "Lengefeld-Rauenstein": null,
- "Lengenfeld(Vogtl)": null,
- "Lengenwang": null,
- "Lengerich(Westf)": null,
- "Lenggries": null,
- "Lenglern": null,
- "Lennestadt-Altenhundem": null,
- "Lennestadt-Grevenbrück": null,
- "Lennestadt-Meggen": null,
- "Lensahn": null,
- "Lentföhrden": null,
- "Leonberg": null,
- "Leopoldshafen Leopoldstr.": null,
- "Leopoldstal": null,
- "Leschede": null,
- "Letmathe": null,
- "Letmathe Dechenhöhle": null,
- "Letschin": null,
- "Lette(Kr Coesfeld)": null,
- "Letter": null,
- "Letter": null,
- "Leubingen": null,
- "Leubsdorf(Rhein)": null,
- "Leubsdorf(Sachs)": null,
- "Leun/Braunfels": null,
- "Leuna Werke Nord": null,
- "Leuna Werke Süd": null,
- "Leutenberg": null,
- "Leuterschach": null,
- "Leutesdorf(Rhein)": null,
- "Leuthen(Cottbus)": null,
- "Leutkirch": null,
- "Leverkusen Chempark": null,
- "Leverkusen Mitte": null,
- "Leverkusen Mitte": null,
- "Leverkusen-Küppersteg": null,
- "Leverkusen-Küppersteg": null,
- "Leverkusen-Rheindorf": null,
- "Leverkusen-Schlebusch": null,
- "Lich(Oberhess)": null,
- "Lichtenberg(Erzgeb)": null,
- "Lichtenfels": null,
- "Lichtenhain(a d Bergbahn)": null,
- "Lichtenstein Ernst-Schneller-Siedlung": null,
- "Lichtenstein Gewerbegebiet": null,
- "Lichtenstein Hartensteiner Straße": null,
- "Lichtenstein(Sachs)": null,
- "Lichtentanne(Sachs)": null,
- "Lichtentanne(Thür)": null,
- "Lichtenthal": null,
- "Liebenthal(Prignitz)": null,
- "Lieblos": null,
- "Liederbach": null,
- "Liederbach-Süd": null,
- "Lietzow(Rügen)": null,
- "Limbach(Vogtl)": null,
- "Limbach(b Homburg,Saar)": null,
- "Limburg Süd": null,
- "Limburg(Lahn)": null,
- "Limburgerhof": null,
- "Limmritz(Sachs)": null,
- "Linda(Elster)": null,
- "Lindach": null,
- "Lindau Hbf": null,
- "Lindau-Aeschach": null,
- "Lindenberg(Mark)": null,
- "Lindenholzhausen": null,
- "Lindern": null,
- "Lindhorst(Schaumb-Lippe)": null,
- "Lindow(Mark)": null,
- "Lindwedel": null,
- "Lingen(Ems)": null,
- "Lingenfeld": null,
- "Linkenheim Rathaus": null,
- "Linnich Bhf": null,
- "Linnich-Tetz": null,
- "Linsburg": null,
- "Linsenhofen": null,
- "Linz(Rhein)": null,
- "Lippstadt": null,
- "Lispenhausen": null,
- "Lissendorf": null,
- "Listerscheid": null,
- "Lobstädt": null,
- "Lochham": null,
- "Lohgarten-Roth": null,
- "Lohhof": null,
- "Lohmen": null,
- "Lohne(Oldb)": null,
- "Lohnweiler": null,
- "Lohr Bahnhof": null,
- "Loitsch-Hohenleuben": null,
- "Lollar": null,
- "Lonsee": null,
- "Loppenhausen": null,
- "Lorch(Rhein)": null,
- "Lorch(Württ)": null,
- "Lorchhausen": null,
- "Lorsbach": null,
- "Lorsch": null,
- "Lottschesee": null,
- "Loxstedt": null,
- "Loßburg-Rodt": null,
- "Lubolz": null,
- "Luckaitztal": null,
- "Luckau-Uckro": null,
- "Luckenau": null,
- "Luckenwalde": null,
- "Ludersheim": null,
- "Ludwigsau-Friedlos": null,
- "Ludwigsburg": null,
- "Ludwigschorgast": null,
- "Ludwigsfelde": null,
- "Ludwigsfelde-Struveshof": null,
- "Ludwigshafen(Bodensee)": null,
- "Ludwigshafen(Rh)Hbf": null,
- "Ludwigshafen(Rh)Hbf": null,
- "Ludwigshafen(Rhein) BASF Mitte": null,
- "Ludwigshafen(Rhein) BASF Nord": null,
- "Ludwigshafen(Rhein) BASF Süd": null,
- "Ludwigshafen(Rhein) Mitte": null,
- "Ludwigshafen-Mundenheim": null,
- "Ludwigshafen-Oggersheim": null,
- "Ludwigshafen-Rheingönheim": null,
- "Ludwigshöhe": null,
- "Ludwigslust": null,
- "Ludwigsstadt": null,
- "Ludwigsthal": null,
- "Luhe": null,
- "Luhe-Wildenau": null,
- "Luisenthal(Saar)": null,
- "Lunden": null,
- "Lunestedt": null,
- "Lutherstadt Eisleben": null,
- "Lutherstadt Wittenberg Altstadt": null,
- "Lutherstadt Wittenberg Hbf": null,
- "Lutherstadt Wittenberg-Labetz": null,
- "Lutherstadt Wittenberg-Piesteritz": null,
- "Lutten": null,
- "Lutum": null,
- "Löbau(Sachs)": null,
- "Löcherberg": null,
- "Löcknitz": null,
- "Lödingsen": null,
- "Löf": null,
- "Löffingen": null,
- "Löhnberg": null,
- "Löhne(Westf)": null,
- "Lörrach Dammstraße": null,
- "Lörrach Hbf": null,
- "Lörrach Museum/Burghof": null,
- "Lörrach Schwarzwaldstraße": null,
- "Lörrach-Brombach/Hauingen": null,
- "Lörrach-Haagen/Messe": null,
- "Lörrach-Stetten": null,
- "Lörzenbach-Fahrenbach": null,
- "Lövenich": null,
- "Lövenich": null,
- "Löwenberg(Mark)": null,
- "Löwental": null,
- "Lößnitz ob Bf": null,
- "Lößnitz unt Bf": null,
- "Lößnitzgrund": null,
- "Lübbecke(Westf)": null,
- "Lübben(Spreewald)": null,
- "Lübbenau(Spreewald)": null,
- "Lübberstedt": null,
- "Lübeck Flughafen": null,
- "Lübeck Hbf": null,
- "Lübeck Hochschulstadtteil": null,
- "Lübeck St Jürgen": null,
- "Lübeck-Dänischburg IKEA": null,
- "Lübeck-Kücknitz": null,
- "Lübeck-Travem. Skandinavienkai": null,
- "Lübeck-Travemünde Hafen": null,
- "Lübeck-Travemünde Strand": null,
- "Lüblow(Meckl)": null,
- "Lübs(Magdeburg)": null,
- "Lübstorf": null,
- "Lüchtringen": null,
- "Lüdenscheid": null,
- "Lüdersdorf(Meckl)": null,
- "Lüdinghausen": null,
- "Lügde": null,
- "Lüneburg": null,
- "Lüneburg": null,
- "Lünen Hbf": null,
- "Lünern": null,
- "Lüssow(Meckl)": null,
- "Lütter": null,
- "Lützel": null,
- "Lützow": null,
- "Maasbüll(b Niebüll)": null,
- "Machern(Sachs)": null,
- "Magdeburg Hasselbachplatz": null,
- "Magdeburg Hbf": null,
- "Magdeburg Herrenkrug": null,
- "Magdeburg SKET Industriepark": null,
- "Magdeburg Südost": null,
- "Magdeburg-Buckau": null,
- "Magdeburg-Eichenweiler": null,
- "Magdeburg-Neustadt": null,
- "Magdeburg-Rothensee": null,
- "Magdeburg-Rothensee": null,
- "Magdeburg-Salbke": null,
- "Magdeburg-Sudenburg": null,
- "Magstadt": null,
- "Mahlow": null,
- "Mahlwinkel": null,
- "Maichingen": null,
- "Maichingen Nord": null,
- "Maikammer-Kirrweiler": null,
- "Mainaschaff": null,
- "Mainhausen Zellhausen": null,
- "Mainleus": null,
- "Mainroth": null,
- "Maintal Ost": null,
- "Maintal West": null,
- "Mainz Hbf": null,
- "Mainz Nord": null,
- "Mainz Römisches Theater": null,
- "Mainz-Bischofsheim": null,
- "Mainz-Bischofsheim": null,
- "Mainz-Gonsenheim": null,
- "Mainz-Gustavsburg": null,
- "Mainz-Kastel": null,
- "Mainz-Laubenheim": null,
- "Mainz-Marienborn": null,
- "Mainz-Mombach": null,
- "Maisach": null,
- "Malchin": null,
- "Malching(Oberbay)": null,
- "Mallersdorf": null,
- "Malmsheim": null,
- "Malsch": null,
- "Malsch Süd": null,
- "Malsfeld": null,
- "Malsfeld-Beiseförth": null,
- "Malter": null,
- "Mammendorf": null,
- "Mandern": null,
- "Manebach": null,
- "Manndorf": null,
- "Mannheim ARENA/Maimarkt": null,
- "Mannheim ARENA/Maimarkt": null,
- "Mannheim Handelshafen": null,
- "Mannheim Hbf": null,
- "Mannheim-Friedrichsfeld Süd": null,
- "Mannheim-Käfertal": null,
- "Mannheim-Luzenberg": null,
- "Mannheim-Neckarau": null,
- "Mannheim-Neckarstadt": null,
- "Mannheim-Rheinau": null,
- "Mannheim-Seckenheim": null,
- "Mannheim-Waldhof": null,
- "Marbach Ost (Villingen-Schwenningen)": null,
- "Marbach West(Villingen-Schwenningen)": null,
- "Marbach(Neckar)": null,
- "Marbach(b Münsingen)": null,
- "Marbach-Grafeneck": null,
- "Marbeck-Heiden": null,
- "Marburg Süd": null,
- "Marburg(Lahn)": null,
- "Maria Rain": null,
- "Maria Veen": null,
- "Marienborn": null,
- "Marienhafe": null,
- "Marienheide": null,
- "Markdorf(Baden)": null,
- "Marke": null,
- "Markelfingen": null,
- "Markelsheim": null,
- "Markkleeberg": null,
- "Markkleeberg Nord": null,
- "Markkleeberg-Gaschwitz": null,
- "Markkleeberg-Großstädteln": null,
- "Markranstädt": null,
- "Marksuhl": null,
- "Markt Bibart": null,
- "Markt Erlbach": null,
- "Markt Indersdorf": null,
- "Markt Schwaben": null,
- "Marktbreit": null,
- "Marktl": null,
- "Marktleuthen": null,
- "Marktoberdorf": null,
- "Marktoberdorf Schule": null,
- "Marktredwitz": null,
- "Marktschorgast": null,
- "Marl Mitte": null,
- "Marl-Hamm": null,
- "Marl-Sinsen": null,
- "Marlishausen": null,
- "Marnheim": null,
- "Marquardt": null,
- "Marsberg": null,
- "Marstetten-Aitrach": null,
- "Martensdorf": null,
- "Martinlamitz": null,
- "Martinroda": null,
- "Martinstein": null,
- "Martinstein": null,
- "Martinszell(Allgäu)": null,
- "Marxgrün": null,
- "Marxzell": null,
- "Marzling": null,
- "Maschen": null,
- "Maselheim": null,
- "Massen": null,
- "Massing": null,
- "Matzing": null,
- "Maubach": null,
- "Mauer(b Heidelberg)": null,
- "Maulbronn Stadt/Kloster": null,
- "Maulbronn West": null,
- "Maulbronn West": null,
- "Maulburg": null,
- "Mausheim": null,
- "Mauthaus": null,
- "Maxau": null,
- "Maxhütte-Haidhof": null,
- "Maximiliansau Eisenbahnstraße": null,
- "Maximiliansau West": null,
- "Maximiliansau-Im Rüsten": null,
- "Mayen Ost": null,
- "Mayen West": null,
- "Mayschoß": null,
- "Mechernich": null,
- "Mechterstädt": null,
- "Meckelfeld": null,
- "Meckenbeuren": null,
- "Meckenheim Industriepark": null,
- "Meckenheim Kottenforst": null,
- "Meckenheim(Bz Köln)": null,
- "Meckesheim": null,
- "Medewitz(Mark)": null,
- "Meeder": null,
- "Meerane": null,
- "Meerbusch-Osterath": null,
- "Meeschensee": null,
- "Mehltheuer": null,
- "Mehrhoog": null,
- "Meine": null,
- "Meinersdorf(Erzgeb)": null,
- "Meinersen": null,
- "Meinerzhagen": null,
- "Meiningen": null,
- "Meinsdorf": null,
- "Meitingen": null,
- "Meitzendorf": null,
- "Meißen": null,
- "Meißen Altstadt": null,
- "Meißen Triebischtal": null,
- "Melbach": null,
- "Melchow": null,
- "Meldorf": null,
- "Melle": null,
- "Mellenbach-Glasbach": null,
- "Mellendorf": null,
- "Mellingen(Thür)": null,
- "Mellrichstadt Bf": null,
- "Melsdorf": null,
- "Melsungen": null,
- "Melsungen Bartenwetzerbrücke": null,
- "Melsungen-Röhrenfurth": null,
- "Melsungen-Röhrenfurth": null,
- "Memmingen": null,
- "Menden(Rheinl)": null,
- "Menden(Sauerland)": null,
- "Menden(Sauerland)Süd": null,
- "Mendig": null,
- "Mengen": null,
- "Mengeringhausen": null,
- "Mengersgereuth-Hämmern": null,
- "Mengersgereuth-Hämmern Ost": null,
- "Menzingen(Baden)": null,
- "Meppen": null,
- "Merching": null,
- "Merchweiler": null,
- "Mering": null,
- "Mering-St Afra": null,
- "Mersch(Westf)": null,
- "Merseburg": null,
- "Merseburg Bergmannsring": null,
- "Merten(Sieg)": null,
- "Mertesheim": null,
- "Mertingen Bahnhof": null,
- "Merzenich": null,
- "Merzig(Saar)": null,
- "Merzig(Saar) Stadtmitte": null,
- "Mesch Neue Mühle": null,
- "Meschede": null,
- "Messel": null,
- "Messinghausen": null,
- "Metelen Land": null,
- "Mettenheim": null,
- "Mettlach": null,
- "Mettmann Stadtwald": null,
- "Mettmann Zentrum": null,
- "Metzingen(Württ)": null,
- "Metzingen-Neuhausen": null,
- "Meuselbach-Schwarzmühle": null,
- "Meyenburg": null,
- "Meßdorf": null,
- "Michelau(Oberfr)": null,
- "Michelau(Württ)": null,
- "Michelaubrück": null,
- "Michelbach(Unterfr)": null,
- "Michelbach(Unterfr) Herrnmühle": null,
- "Michelstadt": null,
- "Michendorf": null,
- "Miedelsbach-Steinenberg": null,
- "Miesbach": null,
- "Miesenbach": null,
- "Miesenheim": null,
- "Mieste": null,
- "Miesterhorst": null,
- "Millingen(b Rees)": null,
- "Millingen(b Rheinb)": null,
- "Miltach": null,
- "Miltenberg": null,
- "Miltern": null,
- "Miltzow": null,
- "Mimberg": null,
- "Mindelaltheim": null,
- "Mindelheim": null,
- "Minden(Westf)": null,
- "Mirow": null,
- "Mistorf": null,
- "Mittel Gründau": null,
- "Mittelherwigsdorf": null,
- "Mitteloelsnitz": null,
- "Mittelschmalkalden": null,
- "Mittelsinn": null,
- "Mittenwald": null,
- "Mittergars": null,
- "Mittweida": null,
- "Mixdorf": null,
- "Mochenwangen": null,
- "Mockrehna": null,
- "Moers": null,
- "Moidentin": null,
- "Monbach-Neuhausen": null,
- "Monreal": null,
- "Monsheim": null,
- "Montabaur": null,
- "Montabaur": null,
- "Monzingen": null,
- "Moorbekhalle": null,
- "Moosbachtal": null,
- "Moosburg": null,
- "Moosrain": null,
- "Moritzburg": null,
- "Morlesau": null,
- "Morsum": null,
- "Mosbach West": null,
- "Mosbach(Baden)": null,
- "Mosbach-Neckarelz": null,
- "Mosel": null,
- "Moselkern": null,
- "Mudersbach": null,
- "Muggensturm": null,
- "Muggensturm Badesee": null,
- "Muhr a See": null,
- "Mulda(Sachs)": null,
- "Muldenberg Floßplatz": null,
- "Muldenhütten": null,
- "Muldenstein": null,
- "Mulsum-Essel": null,
- "Munderkingen": null,
- "Munster(Örtze)": null,
- "Murg(Baden)": null,
- "Murnau": null,
- "Murnau Ort": null,
- "Murrhardt": null,
- "Mußbach": null,
- "Mägdesprung": null,
- "Mägerkingen": null,
- "Möckmühl": null,
- "Mögelin": null,
- "Mögglingen(Gmünd)": null,
- "Möhringen Bahnhof": null,
- "Möhringen Rathaus": null,
- "Mölln(Lauenb)": null,
- "Mölln(Meckl)": null,
- "Mömbris-Mensengesäß": null,
- "Mömbris-Strötzbach": null,
- "Mönchengladbach Hbf": null,
- "Mönchengladbach-Genhausen": null,
- "Mönchengladbach-Lürrip": null,
- "Mönchengladbach-Rheindahlen": null,
- "Mönchhagen": null,
- "Mönchröden": null,
- "Mörfelden": null,
- "Möringen(Altm)": null,
- "Mörlenbach": null,
- "Möser": null,
- "Mössingen": null,
- "Möttingen": null,
- "Mücheln(Geiseltal)": null,
- "Mücheln(Geiseltal) Stadt": null,
- "Mücke(Hess)": null,
- "Müden(Mosel)": null,
- "Mügeln Bf": null,
- "Mügeln Stadt": null,
- "Mühlacker": null,
- "Mühlacker Rößlesweg": null,
- "Mühlanger": null,
- "Mühlbach(Pirna)": null,
- "Mühldorf(Oberbay)": null,
- "Mühlen(Oldb)": null,
- "Mühlen(b Horb)": null,
- "Mühlenbeck-Mönchmühle": null,
- "Mühlhausen(Thür)": null,
- "Mühlhausen(b Engen)": null,
- "Mühlheim(Main)": null,
- "Mühlheim(Main)-Dietesheim": null,
- "Mühlheim(b Tuttlingen)": null,
- "Mühlstetten": null,
- "Mühltal": null,
- "Mühringen": null,
- "Mülheim(Ruhr)Hbf": null,
- "Mülheim(Ruhr)Styrum": null,
- "Mülheim(Ruhr)West": null,
- "Müllheim(Baden)": null,
- "Müllrose": null,
- "Münchberg": null,
- "Müncheberg(Mark)": null,
- "Münchehof(Harz)": null,
- "München Donnersbergerbrücke": null,
- "München Flughafen Besucherpark": null,
- "München Flughafen Terminal": null,
- "München Flughafen Terminal": null,
- "München Hackerbrücke": null,
- "München Harras": null,
- "München Hbf": null,
- "München Hbf (tief)": null,
- "München Hbf Gl.27-36": null,
- "München Hbf Gl.5-10": null,
- "München Heimeranplatz": null,
- "München Heimeranplatz": null,
- "München Hirschgarten": null,
- "München Isartor": null,
- "München Karlsplatz": null,
- "München Leienfelsstr.": null,
- "München Leuchtenbergring": null,
- "München Marienplatz": null,
- "München Ost": null,
- "München Rosenheimer Platz": null,
- "München Siemenswerke": null,
- "München St.Martin-Str.": null,
- "München(Bad Berka)": null,
- "München-Allach": null,
- "München-Aubing": null,
- "München-Berg am Laim": null,
- "München-Daglfing": null,
- "München-Englschalking": null,
- "München-Fasanerie": null,
- "München-Fasangarten": null,
- "München-Feldmoching": null,
- "München-Freiham": null,
- "München-Giesing": null,
- "München-Johanneskirchen": null,
- "München-Karlsfeld": null,
- "München-Laim": null,
- "München-Langwied": null,
- "München-Lochhausen": null,
- "München-Mittersendling": null,
- "München-Moosach": null,
- "München-Neuaubing": null,
- "München-Neuperlach Süd": null,
- "München-Obermenzing": null,
- "München-Pasing": null,
- "München-Pasing": null,
- "München-Perlach": null,
- "München-Riem": null,
- "München-Solln": null,
- "München-Trudering": null,
- "München-Untermenzing": null,
- "München-Westkreuz": null,
- "Münchhausen": null,
- "Münchingen": null,
- "Münchingen Rührberg": null,
- "Münchsmünster": null,
- "Münchweiler(Alsenz)": null,
- "Münchweiler(Rodalb)": null,
- "Münnerstadt": null,
- "Münsingen": null,
- "Münster(W)Zentrum Nord": null,
- "Münster(Westf)Hbf": null,
- "Münster(Westf)Hbf": null,
- "Münster(b Dieburg)": null,
- "Münster-Albachten": null,
- "Münster-Amelsbüren": null,
- "Münster-Hiltrup": null,
- "Münster-Häger": null,
- "Münster-Roxel": null,
- "Münster-Sarmsheim": null,
- "Münster-Sprakel": null,
- "Münstertal(Schwarzwald)": null,
- "Münzesheim": null,
- "Münzesheim Ost": null,
- "Mürlenbach": null,
- "Müssen": null,
- "Nabburg": null,
- "Nachterstedt-Hoym": null,
- "Nackenheim": null,
- "Nagold": null,
- "Nagold Stadtmitte": null,
- "Nagold-Iselshausen": null,
- "Nagold-Steinberg": null,
- "Naila": null,
- "Namborn": null,
- "Namedy": null,
- "Namedy": null,
- "Narsdorf": null,
- "Nassau(Erzgeb)": null,
- "Nassau(Lahn)": null,
- "Nassenbeuren": null,
- "Nassenheide": null,
- "Natrup-Hagen": null,
- "Nauen": null,
- "Nauendorf(Saalkr)": null,
- "Nauheim(b Gr.Gerau)": null,
- "Naumburg(Saale)Hbf": null,
- "Naumburg(Saale)Ost": null,
- "Naumburg-Roßbach": null,
- "Naundorf (b Oschatz)": null,
- "Naunhof": null,
- "Neanderthal": null,
- "Nebitzschen": null,
- "Nebra": null,
- "Nechlin": null,
- "Neckarbischofsheim Helmhof": null,
- "Neckarbischofsheim Nord": null,
- "Neckarbischofsheim Stadt": null,
- "Neckarburken": null,
- "Neckargemünd": null,
- "Neckargemünd Altstadt": null,
- "Neckargerach": null,
- "Neckarhausen bei Neckarsteinach": null,
- "Neckarsteinach": null,
- "Neckarsulm": null,
- "Neckarsulm": null,
- "Neckarsulm Mitte": null,
- "Neckarsulm Nord": null,
- "Neckarsulm Süd": null,
- "Neckarzimmern": null,
- "Neef": null,
- "Neetzendorf": null,
- "Neetzka": null,
- "Neheim-Hüsten": null,
- "Nehren": null,
- "Neidenfels": null,
- "Neidenstein": null,
- "Neinstedt": null,
- "Nellmersbach": null,
- "Nemmenich": null,
- "Nemsdorf-Göhrendorf": null,
- "Nendingen(b Tuttlingen)": null,
- "Nennhausen": null,
- "Nennig": null,
- "Nennigmühle": null,
- "Nenzingen": null,
- "Nersingen": null,
- "Nesselwang": null,
- "Nettersheim": null,
- "Netzeband": null,
- "Netzkater": null,
- "Netzschkau": null,
- "Neu Isenburg": null,
- "Neu Isenburg": null,
- "Neu Pudagla": null,
- "Neu St Jürgen": null,
- "Neu Wokern": null,
- "Neu Wulmstorf": null,
- "Neu-Anspach": null,
- "Neu-Edingen/Friedrichsfeld": null,
- "Neu-Ulm": null,
- "Neubeckum": null,
- "Neubeckum": null,
- "Neubiberg": null,
- "Neubrandenburg": null,
- "Neubrücke(Nahe)": null,
- "Neubukow": null,
- "Neuburg(Donau)": null,
- "Neuburg(Kammel)": null,
- "Neuburg(Rhein)": null,
- "Neubäu": null,
- "Neudenau": null,
- "Neudietendorf": null,
- "Neudorf(Erzgeb)": null,
- "Neudorf-Platendorf": null,
- "Neue Schenke": null,
- "Neuenburg(Baden)": null,
- "Neuenbürg(Enz)": null,
- "Neuenbürg(Enz) Eyachbrücke": null,
- "Neuenbürg(Enz) Freibad": null,
- "Neuenbürg(Enz) Süd": null,
- "Neuendettelsau": null,
- "Neuenhagen(b Berlin)": null,
- "Neuenkirchen(Oldb)": null,
- "Neuenmarkt-Wirsberg": null,
- "Neuenrade": null,
- "Neuenstein": null,
- "Neufahrn(Niederbay)": null,
- "Neufahrn(b Freising)": null,
- "Neuffen": null,
- "Neufra(Hohenz)": null,
- "Neugersdorf": null,
- "Neugilching": null,
- "Neuhaus am Rennweg": null,
- "Neuhaus(Pegnitz)": null,
- "Neuhaus-Igelshieb": null,
- "Neuhausen Bad Bf": null,
- "Neuhausen(Cottbus)": null,
- "Neuhausen(b Landshut)": null,
- "Neuhof(Kr Fulda)": null,
- "Neuhof(b Zossen)": null,
- "Neukieritzsch": null,
- "Neukirch(Lausitz)Ost": null,
- "Neukirch(Lausitz)West": null,
- "Neukirchen(Inn)": null,
- "Neukirchen(b Sulzb)": null,
- "Neukirchen-Klaffenbach": null,
- "Neukirchen-Wyhra": null,
- "Neukloster(Kr Stade)": null,
- "Neulußheim": null,
- "Neumark(Sachs)": null,
- "Neumarkt(Oberpf)": null,
- "Neumarkt-St Veit": null,
- "Neumühle(Elster)": null,
- "Neumünster": null,
- "Neumünster Stadtwald": null,
- "Neumünster Süd AKN": null,
- "Neundorf(Anh)": null,
- "Neunhofen": null,
- "Neunkirch": null,
- "Neunkirchen a Sand": null,
- "Neunkirchen(Kr Siegen)": null,
- "Neunkirchen(Saar)-Wellesweiler": null,
- "Neunkirchen(Saar)Hbf": null,
- "Neuoelsnitz": null,
- "Neupetershain": null,
- "Neuruppin Rheinsberger Tor": null,
- "Neuruppin West": null,
- "Neuruppin West": null,
- "Neusalza-Spremberg": null,
- "Neuses(b Kronach)": null,
- "Neusorg": null,
- "Neuss Allerheiligen": null,
- "Neuss Am Kaiser": null,
- "Neuss Hbf": null,
- "Neuss Hbf": null,
- "Neuss Hbf": null,
- "Neuss Rheinparkcenter": null,
- "Neuss Süd": null,
- "Neustadt am Rübenberge": null,
- "Neustadt(Aisch)Bahnhof": null,
- "Neustadt(Aisch)Mitte": null,
- "Neustadt(Donau)": null,
- "Neustadt(Dosse)": null,
- "Neustadt(Holst)": null,
- "Neustadt(Holst)Gbf": null,
- "Neustadt(Kr Marburg)": null,
- "Neustadt(Orla)": null,
- "Neustadt(Sachs)": null,
- "Neustadt(Schwarzw)": null,
- "Neustadt(Waldnaab)": null,
- "Neustadt(Weinstr) Süd": null,
- "Neustadt(Weinstr)Hbf": null,
- "Neustadt(b Coburg)": null,
- "Neustadt-Böbig": null,
- "Neustadt-Glewe": null,
- "Neustadt-Hohenacker": null,
- "Neustift(b Passau)": null,
- "Neustrelitz Hbf": null,
- "Neusäß": null,
- "Neusörnewitz": null,
- "Neutrebbin": null,
- "Neuwied": null,
- "Neuwirtshaus(Porscheplatz)": null,
- "Neuzelle": null,
- "Neuötting": null,
- "Nidda": null,
- "Nidderau": null,
- "Nidderau": null,
- "Nidderau-Eichen": null,
- "Nidderau-Windecken": null,
- "Nideggen-Brück": null,
- "Niebüll": null,
- "Niebüll neg": null,
- "Niebüll, Sylt Shuttle": null,
- "Niedaltdorf": null,
- "Nieder Flörsheim-Dalsheim": null,
- "Nieder Ohmen": null,
- "Nieder Olm": null,
- "Nieder Wöllstadt": null,
- "Niederau": null,
- "Niederau-Tuchmühle": null,
- "Niederbiegen": null,
- "Niederbobritzsch": null,
- "Niederbrechen": null,
- "Niederdollendorf": null,
- "Niederdorf(Erzgeb)": null,
- "Niederdorfelden": null,
- "Niederdreisbach": null,
- "Niederdresselndorf": null,
- "Niedererbach": null,
- "Niederfinow": null,
- "Niedergörsdorf": null,
- "Niederhadamar": null,
- "Niederheimbach": null,
- "Niederhöchstadt": null,
- "Niederhövels": null,
- "Niederjosbach": null,
- "Niederlahnstein": null,
- "Niederlehme": null,
- "Niederlindhart": null,
- "Niederlinxweiler": null,
- "Niedermittlau": null,
- "Niedermohr": null,
- "Niederndodeleben": null,
- "Niedernhausen(Taunus)": null,
- "Niederoderwitz": null,
- "Niederpöllnitz": null,
- "Niederraunau": null,
- "Niederroth": null,
- "Niedersachswerfen": null,
- "Niedersachswerfen Herkulesmarkt": null,
- "Niedersachswerfen Ilfelder Straße": null,
- "Niedersachswerfen Ost": null,
- "Niederscheld(Dillkr)Süd": null,
- "Niederschelden": null,
- "Niederschelden Nord": null,
- "Niederschlag": null,
- "Niederschlottwitz": null,
- "Niederschmalkalden": null,
- "Niederselters": null,
- "Niederspier": null,
- "Niedersteinbach": null,
- "Niederstetten": null,
- "Niederstotzingen": null,
- "Niedertrebra": null,
- "Niederwalgern": null,
- "Niederwalluf": null,
- "Niederwartha": null,
- "Niederweimar": null,
- "Niederwiesa": null,
- "Niederwillingen": null,
- "Niederwinden": null,
- "Niederwürschnitz": null,
- "Niederzeuzheim": null,
- "Niederzissen": null,
- "Niederzwönitz": null,
- "Niefern": null,
- "Niemberg": null,
- "Nienburg(Saale)": null,
- "Nienburg(Weser)": null,
- "Nienhagen(Halberst)": null,
- "Nierstein": null,
- "Nieukerk": null,
- "Nievenheim": null,
- "Nievern": null,
- "Niklashausen": null,
- "Nimburg(Baden)": null,
- "Nistertal-Bad Marienberg": null,
- "Nittel": null,
- "Nohen": null,
- "Nohfelden": null,
- "Nohra(Weimar)": null,
- "Nohra(Wipper)": null,
- "Nonnenhorn": null,
- "Nordbögge": null,
- "Norddeich": null,
- "Norddeich Mole": null,
- "Norden": null,
- "Nordendorf": null,
- "Nordenham": null,
- "Norderstedt Mitte": null,
- "Nordhalben Bf": null,
- "Nordhastedt": null,
- "Nordhausen": null,
- "Nordhausen Bahnhofsplatz": null,
- "Nordhausen Hesseröder Straße": null,
- "Nordhausen Nord": null,
- "Nordhausen Ricarda-Huch-Straße": null,
- "Nordhausen Schurzfell": null,
- "Nordhausen-Altentor": null,
- "Nordhausen-Krimderode": null,
- "Nordhausen-Salza": null,
- "Nordheim(Württ)": null,
- "Nordholz": null,
- "Nordsode": null,
- "Nordstemmen": null,
- "Nordwalde": null,
- "Norf": null,
- "Norheim": null,
- "Norsingen": null,
- "Northeim(Han)": null,
- "Nortorf": null,
- "Nossentin": null,
- "Nottuln-Appelhülsen": null,
- "Nufringen": null,
- "Nußberg-Schönau": null,
- "Nöbdenitz": null,
- "Nördlingen": null,
- "Nörten-Hardenberg": null,
- "Nünchritz": null,
- "Nürnberg Frankenstadion": null,
- "Nürnberg Frankenstadion Sonderbahnsteig": null,
- "Nürnberg Hbf": null,
- "Nürnberg Nordost": null,
- "Nürnberg Ost": null,
- "Nürnberg Ostring": null,
- "Nürnberg Rothenburger Str.": null,
- "Nürnberg-Dutzendteich": null,
- "Nürnberg-Dürrenhof": null,
- "Nürnberg-Eibach": null,
- "Nürnberg-Erlenstegen": null,
- "Nürnberg-Gleißhammer": null,
- "Nürnberg-Laufamholz": null,
- "Nürnberg-Mögeldorf": null,
- "Nürnberg-Rehhof": null,
- "Nürnberg-Reichelsdorf": null,
- "Nürnberg-Reichelsdorf": null,
- "Nürnberg-Sandreuth": null,
- "Nürnberg-Schweinau": null,
- "Nürnberg-Stein": null,
- "Nürnberg-Steinbühl": null,
- "Nürnberg-Steinbühl": null,
- "Nürtingen": null,
- "Nürtingen-Roßdorf": null,
- "Nürtingen-Vorstadt": null,
- "Nützen": null,
- "Ober Ramstadt": null,
- "Ober Widdersheim": null,
- "Oberachern": null,
- "Oberachern Bindfadenfabrik": null,
- "Oberaichen": null,
- "Oberammergau": null,
- "Oberasbach": null,
- "Oberau": null,
- "Oberaudorf": null,
- "Oberbettingen-Hillesheim": null,
- "Oberbillig": null,
- "Oberbimbach": null,
- "Oberboihingen": null,
- "Oberbrechen": null,
- "Oberdachstetten": null,
- "Oberderdingen-Flehingen Industrie": null,
- "Oberelchingen": null,
- "Oberelsungen": null,
- "Obererbach": null,
- "Oberesslingen": null,
- "Oberferrieden": null,
- "Obergimpern": null,
- "Obergries": null,
- "Obergriesbach": null,
- "Obergrunstedt": null,
- "Oberhaid": null,
- "Oberharmersbach Dorf": null,
- "Oberharmersbach-Riersbach": null,
- "Oberhausen Hbf": null,
- "Oberhausen-Holten": null,
- "Oberhausen-Osterfeld Süd": null,
- "Oberhausen-Osterfeld Süd": null,
- "Oberhausen-Sterkrade": null,
- "Oberhof(Thür)": null,
- "Oberholz": null,
- "Oberkirch": null,
- "Oberkirch-Köhlersiedlung": null,
- "Oberkochen": null,
- "Oberkotzau": null,
- "Oberkrozingen": null,
- "Oberlahnstein": null,
- "Oberlauscha": null,
- "Oberlenningen": null,
- "Oberlichtenau": null,
- "Oberlindhart": null,
- "Oberlinxweiler": null,
- "Obermaubach": null,
- "Obermohr": null,
- "Obernau": null,
- "Obernburg-Elsenfeld": null,
- "Oberndorf(Neckar)": null,
- "Oberndorf(Wittgenstein)": null,
- "Obernhof(Lahn)": null,
- "Oberoderwitz": null,
- "Oberoderwitz Oberdorf": null,
- "Oberottmarshausen": null,
- "Oberrohn": null,
- "Oberrothenbach": null,
- "Oberrotweil": null,
- "Oberröblingen": null,
- "Oberschefflenz": null,
- "Oberschleißheim": null,
- "Oberschlottwitz": null,
- "Obersdorf": null,
- "Obersinn": null,
- "Oberstaufen": null,
- "Oberstdorf": null,
- "Obertraubling": null,
- "Obertshausen(Kr Of)": null,
- "Obertsrot": null,
- "Oberursel(Taunus)": null,
- "Oberursel-Stierstadt": null,
- "OberurselWeißkirchen/Steinbach": null,
- "Obervogelgesang": null,
- "Oberweimar": null,
- "Oberweißbach-Deesbach": null,
- "Oberwerrn": null,
- "Oberwesel": null,
- "Oberwinden": null,
- "Oberwinter": null,
- "Oberzell": null,
- "Oberzissen": null,
- "Oberöwisheim": null,
- "Obing": null,
- "Obstfelderschmiede": null,
- "Ochenbruck": null,
- "Ochsenfurt": null,
- "Ochsenhausen": null,
- "Ochtmersleben": null,
- "Ochtrup": null,
- "Ockenheim": null,
- "Odenheim Bf": null,
- "Odenheim West": null,
- "Oderin": null,
- "Oebisfelde": null,
- "Oederan": null,
- "Oegeln": null,
- "Oehna": null,
- "Oelde": null,
- "Oelsnitz Bahnhofstraße": null,
- "Oelsnitz(Erzgeb)": null,
- "Oelsnitz(Vogtl)": null,
- "Oerel": null,
- "Oerlenbach": null,
- "Oerlinghausen": null,
- "Oertzenhof": null,
- "Oesede": null,
- "Oestrich-Winkel": null,
- "Oettingen(Bay)": null,
- "Oeventrop": null,
- "Offenau": null,
- "Offenbach(Main) Kaiserlei": null,
- "Offenbach(Main) Ledermuseum": null,
- "Offenbach(Main) Marktplatz": null,
- "Offenbach(Main)Hbf": null,
- "Offenbach(Main)Ost": null,
- "Offenbach-Bieber": null,
- "Offenbach-Waldhof": null,
- "Offenburg": null,
- "Offenburg Kreisschulzentrum": null,
- "Offenhausen": null,
- "Offensen(Kr North)": null,
- "Offingen": null,
- "Oftersheim": null,
- "Ohlstadt": null,
- "Okarben": null,
- "Oker": null,
- "Olbernhau": null,
- "Olbernhau West": null,
- "Olbernhau-Grünthal": null,
- "Olbersdorf Niederdorf": null,
- "Olbersdorf Oberdorf": null,
- "Olbersleben-Ellersleben": null,
- "Olching": null,
- "Oldenburg(Holst)": null,
- "Oldenburg(Oldb)": null,
- "Oldenburg-Wechloy": null,
- "Oldenbüttel": null,
- "Oldentrup": null,
- "Olpe": null,
- "Olsberg": null,
- "Olsbrücken": null,
- "Opladen": null,
- "Opladen": null,
- "Oppenau": null,
- "Kiel-Oppendorf": null,
- "Oppenheim": null,
- "Oppenweiler(Württ)": null,
- "Oppurg": null,
- "Oranienbaum(Anh)": null,
- "Oranienburg": null,
- "Orlamünde": null,
- "Orschweier": null,
- "Ortrand": null,
- "Oschatz": null,
- "Oschatz Körnerstr": null,
- "Oschatz Lichtstr": null,
- "Oschatz Südbf": null,
- "Oschersleben(Bode)": null,
- "Osnabrück Altstadt": null,
- "Osnabrück Hbf": null,
- "Osnabrück Hbf": null,
- "Osnabrück Hbf": null,
- "Osnabrück-Sutthausen": null,
- "Ostbevern": null,
- "Osterburg": null,
- "Osterburken": null,
- "Osterhofen(Nby)": null,
- "Osterhofen(Oberbay)": null,
- "Osterholz-Scharmbeck": null,
- "Ostermünchen": null,
- "Osternienburg": null,
- "Osterode am Harz Leege": null,
- "Osterode am Harz Mitte": null,
- "Ostersode": null,
- "Osterspai": null,
- "Osterstedt": null,
- "Osterteich": null,
- "Osterwald": null,
- "Osterweddingen": null,
- "Ostheim v Rhön": null,
- "Ostheim(Kr Hanau)": null,
- "Ostheim(b Butzbach)": null,
- "Osthofen": null,
- "Ostrach Bahnhof": null,
- "Ostrau": null,
- "Ostseebad Binz": null,
- "Ostseebad Kühlungsborn Mitte": null,
- "Ostseebad Kühlungsborn Ost": null,
- "Ostseebad Kühlungsborn West": null,
- "Ottbergen": null,
- "Ottenau": null,
- "Ottendorf(Mittweida)": null,
- "Ottendorf-Okrilla Hp": null,
- "Ottendorf-Okrilla Nord": null,
- "Ottendorf-Okrilla Süd": null,
- "Ottenhofen(Oberbay)": null,
- "Ottenhofen-Bergel": null,
- "Ottenhöfen": null,
- "Ottenhöfen West": null,
- "Ottensoos": null,
- "Otterfing": null,
- "Otterndorf": null,
- "Ottersberg(Han)": null,
- "Otterwisch": null,
- "Otting": null,
- "Otting-Weilheim": null,
- "Ottobrunn": null,
- "Ottweiler(Saar)": null,
- "Otzberg Lengfeld": null,
- "Otze": null,
- "Ovelgünne": null,
- "Overath": null,
- "Owen(Teck)": null,
- "Owschlag": null,
- "Oy-Mittelberg": null,
- "Oßmannstedt": null,
- "Paderborn Hbf": null,
- "Paderborn Kasseler Tor": null,
- "Paderborn Nord": null,
- "Paderborn-Schloss Neuhaus": null,
- "Paffendorf": null,
- "Paindorf": null,
- "Palzem": null,
- "Pankofen": null,
- "Pansdorf": null,
- "Papenburg(Ems)": null,
- "Papendorf": null,
- "Papiermühle(Stadtr)": null,
- "Pappenheim": null,
- "Parchim": null,
- "Parkentin": null,
- "Parsberg": null,
- "Partenstein": null,
- "Pasewalk": null,
- "Pasewalk Ost": null,
- "Passau Hbf": null,
- "Passow(Uckermark)": null,
- "Patersdorf": null,
- "Paulinenaue": null,
- "Paulinenaue": null,
- "Paulinzella": null,
- "Pechbrunn": null,
- "Peenemünde": null,
- "Pegau": null,
- "Pegnitz": null,
- "Peine": null,
- "Peiting Nord": null,
- "Peiting Ost": null,
- "Peitz Ost": null,
- "Peiß": null,
- "Peißen": null,
- "Peißenberg": null,
- "Peißenberg Nord": null,
- "Penzberg": null,
- "Perkam": null,
- "Perl": null,
- "Perleberg": null,
- "Petergrube": null,
- "Petersaurach": null,
- "Petersaurach Nord": null,
- "Petershagen Nord": null,
- "Petershagen(Uckerm)": null,
- "Petershagen-Lahde": null,
- "Petershausen(Obb)": null,
- "Petersroda": null,
- "Pfaffenhain": null,
- "Pfaffenhausen": null,
- "Pfaffenhofen(Ilm)": null,
- "Pfalzel": null,
- "Pfarrkirchen": null,
- "Pfeddersheim": null,
- "Pfettrach": null,
- "Pflaumloch": null,
- "Pforzheim Hbf": null,
- "Pforzheim Maihälden": null,
- "Pforzheim-Weißenstein": null,
- "Pfreimd": null,
- "Pfronten-Ried": null,
- "Pfronten-Steinach": null,
- "Pfronten-Weißbach": null,
- "Pfullendorf": null,
- "Pfungstadt": null,
- "Pfäffingen": null,
- "Philippsburg(Baden)": null,
- "Philippshagen": null,
- "Philippsheim": null,
- "Piding": null,
- "Pillgram": null,
- "Pinneberg": null,
- "Pinneberg": null,
- "Pinnow(Uckermark)": null,
- "Pinzberg": null,
- "Pirk": null,
- "Pirmasens Hbf": null,
- "Pirmasens Nord": null,
- "Pirna": null,
- "Pirna-Copitz": null,
- "Pirna-Copitz Nord": null,
- "Pittenhart": null,
- "Plaaz": null,
- "Plaidt": null,
- "Planegg": null,
- "Plate(Meckl)": null,
- "Plattling": null,
- "Plaue(Thür)": null,
- "Plauen(Vogtl) Mitte": null,
- "Plauen(Vogtl) ob Bf": null,
- "Plauen(Vogtl)-Straßberg": null,
- "Plauen(Vogtl)West": null,
- "Pleinfeld": null,
- "Plessa": null,
- "Plettenberg": null,
- "Plochingen": null,
- "Plön": null,
- "Plüderhausen": null,
- "Plüschow": null,
- "Pockau-Lengefeld": null,
- "Pocking": null,
- "Poggenhagen": null,
- "Poikam": null,
- "Poing": null,
- "Pommelsbrunn": null,
- "Pommern(Mosel)": null,
- "Pommritz": null,
- "Ponitz": null,
- "Poppenhausen": null,
- "Porschdorf(Pirna)": null,
- "Porstendorf": null,
- "Porta Westfalica": null,
- "Porz(Rhein)": null,
- "Porz(Rhein)": null,
- "Porz-Wahn": null,
- "Posewald": null,
- "Posewald": null,
- "Possenhofen": null,
- "Postbauer-Heng": null,
- "Potsdam Charlottenhof": null,
- "Potsdam Griebnitzsee": null,
- "Potsdam Hbf": null,
- "Potsdam Medienstadt Babelsberg": null,
- "Potsdam Park Sanssouci": null,
- "Potsdam Pirschheide": null,
- "Potsdam-Babelsberg": null,
- "Potsdam-Rehbrücke": null,
- "Praest": null,
- "Pratau": null,
- "Preetz": null,
- "Premnitz Nord": null,
- "Premnitz Zentrum": null,
- "Prenzlau": null,
- "Pressath": null,
- "Pressig-Rothenkirchen": null,
- "Pretzfeld": null,
- "Pretzier(Altm)": null,
- "Pretzsch": null,
- "Preußen": null,
- "Priemerburg": null,
- "Prien a Chiemsee": null,
- "Priestewitz": null,
- "Priort": null,
- "Prisdorf": null,
- "Prittitz": null,
- "Pritzerbe": null,
- "Pritzier": null,
- "Pritzwalk": null,
- "Pritzwalk Hainholz": null,
- "Pritzwalk West": null,
- "Probsteierhagen": null,
- "Probstzella": null,
- "Profen": null,
- "Prora": null,
- "Prora Ost": null,
- "Prosselsheim": null,
- "Prödel": null,
- "Prösen": null,
- "Prösen Ost": null,
- "Prösen West": null,
- "Puchheim": null,
- "Pulheim": null,
- "Pullach": null,
- "Pulling(b Freising)": null,
- "Pulsnitz": null,
- "Pulsnitz Süd": null,
- "Puschendorf": null,
- "Putbus": null,
- "Puttgarden": null,
- "Putzkau": null,
- "Pölchow": null,
- "Pölling": null,
- "Pönitz(Holst)": null,
- "Pönitz(Leipzig)": null,
- "Pösing": null,
- "Pößneck ob Bf": null,
- "Pößneck unt Bf": null,
- "Quadrath-Ichendorf": null,
- "Quakenbrück": null,
- "Quedlinburg": null,
- "Quedlinburg-Quarmbeck": null,
- "Quelle": null,
- "Quelle": null,
- "Quelle": null,
- "Quelle-Kupferheide": null,
- "Querfurt": null,
- "Quickborn": null,
- "Quickborn Süd": null,
- "Quickborner Straße": null,
- "Quierschied": null,
- "Quint": null,
- "Rabenau": null,
- "Rackith(Elbe)": null,
- "Rackwitz(Leipzig)": null,
- "Radbruch": null,
- "Raddusch": null,
- "Radeberg": null,
- "Radebeul Ost": null,
- "Radebeul Ost": null,
- "Radebeul-Kötzschenbroda": null,
- "Radebeul-Naundorf": null,
- "Radebeul-Weintraube": null,
- "Radebeul-Zitzschewig": null,
- "Radeburg": null,
- "Radersdorf": null,
- "Radis": null,
- "Radldorf(Niederbay)": null,
- "Radolfzell": null,
- "Raestrup-Everswinkel": null,
- "Raguhn": null,
- "Rahden(Kr Lübbecke)": null,
- "Rain": null,
- "Raindorf": null,
- "Raisdorf": null,
- "Raisdorf": null,
- "Raisting": null,
- "Raitersaich": null,
- "Rakow": null,
- "Rambin(Rügen)": null,
- "Ramerberg": null,
- "Rammelsbach": null,
- "Rammingen(Bay)": null,
- "Rammingen(Württ)": null,
- "Ramsbach Birkhof": null,
- "Ramsbach Höfle": null,
- "Ramsberg": null,
- "Ramsen": null,
- "Ramsenthal": null,
- "Ramstein": null,
- "Rangendingen": null,
- "Rangsdorf": null,
- "Ranstadt": null,
- "Rastatt": null,
- "Rastatt Beinle": null,
- "Rastede": null,
- "Rastow": null,
- "Rathenow": null,
- "Rathmannsdorf(Kr Pirna)": null,
- "Ratingen Ost": null,
- "Ratzeburg": null,
- "Raubling": null,
- "Rauenstein(Thür)": null,
- "Raumland-Markhausen": null,
- "Raumünzach": null,
- "Raun": null,
- "Raunheim": null,
- "Ravensburg": null,
- "Rebdorf-Hofmühle": null,
- "Rech": null,
- "Rechenberg": null,
- "Rechtenstein": null,
- "Rechterfeld": null,
- "Reckendorf": null,
- "Reckenfeld": null,
- "Recklinghausen Hbf": null,
- "Recklinghausen Süd": null,
- "Reckweilerhof": null,
- "Reddelich": null,
- "Rednitzhembach": null,
- "Redwitz(Rodach)": null,
- "Regen": null,
- "Regensburg Hbf": null,
- "Regensburg-Burgweinting": null,
- "Regensburg-Prüfening": null,
- "Regensburg-Prüfening": null,
- "Regenstauf": null,
- "Regis-Breitingen": null,
- "Rehau": null,
- "Rehfeld(Falkenberg)": null,
- "Rehfelde": null,
- "Rehna": null,
- "Rehweiler": null,
- "Reichelsdorfer Keller": null,
- "Reichelsheim(Wett)": null,
- "Reichenau(Baden)": null,
- "Reichenbach(Fils)": null,
- "Reichenbach(Oberlausitz)": null,
- "Reichenbach(Vogtl) ob Bf": null,
- "Reichenbach(b. Ettlingen)": null,
- "Reichenberg(Unterfr)": null,
- "Reichenschwand": null,
- "Reichersbeuern": null,
- "Reichertshausen(Ilm)": null,
- "Reicholzheim": null,
- "Reifland-Wünschendorf": null,
- "Reihen": null,
- "Reil": null,
- "Reilsheim": null,
- "Reinbek": null,
- "Reinfeld(Holst)": null,
- "Reinhardsbrunn-Friedrichroda": null,
- "Reinheim(Odenw)": null,
- "Reinsbüttel": null,
- "Reinsdorf(Artern)": null,
- "Reinsdorf(bei Nebra)": null,
- "Reinstetten": null,
- "Reisen(Hess)": null,
- "Reiskirchen(Kr Gi)": null,
- "Reken": null,
- "Remagen": null,
- "Remscheid Hbf": null,
- "Remscheid-Güldenwerth": null,
- "Remscheid-Lennep": null,
- "Remscheid-Lüttringhausen": null,
- "Renchen": null,
- "Rendsburg": null,
- "Renningen": null,
- "Renningen Süd": null,
- "Rennsteig": null,
- "Rentrisch": null,
- "Rentweinsdorf": null,
- "Rentwertshausen": null,
- "Rentzschmühle": null,
- "Rethen(Leine)": null,
- "Retzbach-Zellingen": null,
- "Reurieth": null,
- "Reuterstadt Stavenhagen": null,
- "Reuth(b Erbendorf)": null,
- "Reuth(b Plauen,Vogtl)": null,
- "Reutlingen Hbf": null,
- "Reutlingen West": null,
- "Reutlingen-Betzingen": null,
- "Reutlingen-Sondelfingen": null,
- "Reußen": null,
- "Rhade": null,
- "Rheda-Wiedenbrück": null,
- "Rheinbach": null,
- "Rheinbach Römerkanal": null,
- "Rheinberg(Rheinl)": null,
- "Rheinbrohl": null,
- "Rheine": null,
- "Rheine-Mesum": null,
- "Rheinfelden(Baden)": null,
- "Rheinhausen": null,
- "Rheinhausen Ost": null,
- "Rheinsberg(Mark)": null,
- "Rheinsheim": null,
- "Rheinweiler": null,
- "Rheinzabern Alte Römerstraße": null,
- "Rheinzabern Bf": null,
- "Rheinzabern Rappengasse": null,
- "Rhens": null,
- "Rheydt Hbf": null,
- "Rheydt Hbf": null,
- "Rheydt-Odenkirchen": null,
- "Rhöndorf": null,
- "Ribnitz-Damgarten Ost": null,
- "Ribnitz-Damgarten West": null,
- "Richen(b Eppingen)": null,
- "Rickling": null,
- "Ried": null,
- "Riederau": null,
- "Riedlingen": null,
- "Riedrode": null,
- "Riedstadt-Goddelau": null,
- "Riedstadt-Wolfskehlen": null,
- "Riegel am Kaiserstuhl Ort": null,
- "Riegel-Malterd.NE": null,
- "Riegel-Malterdingen": null,
- "Riehen": null,
- "Riehen Niederholz": null,
- "Rieneck": null,
- "Riesa": null,
- "Rieschweiler": null,
- "Rieseby": null,
- "Rieste": null,
- "Riestedt": null,
- "Rietheim(Württ)": null,
- "Rietschen": null,
- "Rimbach": null,
- "Ringleben-Gebesee": null,
- "Ringsheim": null,
- "Rinkerode": null,
- "Rinklingen": null,
- "Rinnthal": null,
- "Rinteln": null,
- "Rippberg": null,
- "Ritschenhausen": null,
- "Ritterhude": null,
- "Rockenhausen": null,
- "Rodalben": null,
- "Rodenbach(Dillkr)": null,
- "Rodenbach(b Hanau)": null,
- "Rodenkirchen(Oldb)": null,
- "Rodewisch": null,
- "Rodgau-Dudenhofen": null,
- "Rodgau-Hainhausen": null,
- "Rodgau-Jügesheim": null,
- "Rodgau-Nieder Roden": null,
- "Rodgau-Rollwald": null,
- "Rodgau-Weiskirchen": null,
- "Rodheim v d Höhe": null,
- "Roding": null,
- "Rodleben": null,
- "Roggentin": null,
- "Rohr(Thür)": null,
- "Rohrbach(Ilm)": null,
- "Rohrbach(Oberbay)": null,
- "Rohrbach(Pfalz)": null,
- "Rohrbach(Saar)": null,
- "Rohrenfeld": null,
- "Roigheim": null,
- "Roisdorf": null,
- "Roitzsch(Bitterf)": null,
- "Rolandseck": null,
- "Rollhofen": null,
- "Rommelshausen": null,
- "Rommerskirchen": null,
- "Ronneburg(Thür)": null,
- "Ronnenberg": null,
- "Ronshausen": null,
- "Rosbach v d Höhe": null,
- "Rosbach(Sieg)": null,
- "Rosenau(b Grafenau)": null,
- "Rosenberg(Baden)": null,
- "Rosendahl-Holtwick": null,
- "Rosenheim": null,
- "Rosenheim Hochschule": null,
- "Rosenwinkel": null,
- "Rostock Hbf": null,
- "Rostock Holbeinplatz": null,
- "Rostock Parkstraße": null,
- "Rostock Thierfelder Str.": null,
- "Rostock-Bramow": null,
- "Rostock-Evershagen": null,
- "Rostock-Kassebohm": null,
- "Rostock-Lichtenhagen": null,
- "Rostock-Lütten Klein": null,
- "Rostock-Marienehe": null,
- "Rostock-Torfbrücke": null,
- "Rot am See": null,
- "Rot-Malsch": null,
- "Rotenbach(Enz)": null,
- "Rotenburg a.d. Fulda": null,
- "Rotenburg(Wümme)": null,
- "Rotenhain": null,
- "Roth": null,
- "Rothenburg ob der Tauber": null,
- "Rothenbürg": null,
- "Rothenstein(Saale)": null,
- "Rott(Inn)": null,
- "Rottenacker": null,
- "Rottenbach": null,
- "Rottenburg(Neckar)": null,
- "Rottendorf": null,
- "Rottershausen": null,
- "Rottweil": null,
- "Rottweil Göllsdorf": null,
- "Rottweil Neufra": null,
- "Rottweil Saline": null,
- "Roßbach(Pfalz)": null,
- "Roßla": null,
- "Roßlau(Elbe)": null,
- "Roßtal": null,
- "Roßtal Wegbrücke": null,
- "Rudersberg": null,
- "Rudersberg Nord": null,
- "Rudersberg-Oberndorf": null,
- "Rudersdorf(Siegen)": null,
- "Rudolstadt(Thür)": null,
- "Rudolstadt-Schwarza": null,
- "Ruhland": null,
- "Ruhlsdorf-Zerpenschleuse": null,
- "Ruhmannsfelden": null,
- "Ruhpolding": null,
- "Ruhstorf": null,
- "Rumeln": null,
- "Rummenohl": null,
- "Runkel": null,
- "Ruppertsgrün": null,
- "Rupprechtstegen": null,
- "Ruschberg": null,
- "Ruschwedel": null,
- "Rutesheim": null,
- "Ruthenbeck": null,
- "Rätzlingen": null,
- "Röblingen am See": null,
- "Rödental": null,
- "Rödental Mitte": null,
- "Rödermark-Ober Roden": null,
- "Rödermark-Urberach": null,
- "Rödlitz-Hohndorf": null,
- "Röhrmoos": null,
- "Röhrnbach": null,
- "Rönshausen": null,
- "Röntgental": null,
- "Röslau": null,
- "Rösrath": null,
- "Rösrath-Stümpen": null,
- "Röt": null,
- "Rötenbach(Baden)": null,
- "Rötgesbüttel": null,
- "Röthenbach(Allgäu)": null,
- "Röthenbach(Oberpf)": null,
- "Röthenbach(Pegnitz)": null,
- "Röthenbach-Seespitze": null,
- "Röthenbach-Steinberg": null,
- "Rövershagen": null,
- "Rückersbacher Schlucht": null,
- "Rückersdorf": null,
- "Rückersdorf(Mfr)": null,
- "Rüdesheim(Rhein)": null,
- "Rüdnitz": null,
- "Rülzheim Bf": null,
- "Rülzheim Freizeitzentrum": null,
- "Rümmingen": null,
- "Ründeroth": null,
- "Rüsselbach": null,
- "Rüsselsheim": null,
- "Rüsselsheim Opelwerk": null,
- "Saal(Donau)": null,
- "Saalburg(Taunus)": null,
- "Saalfeld(Saale)": null,
- "Saarbrücken Hbf": null,
- "Saarbrücken Ost": null,
- "Saarbrücken-Burbach": null,
- "Saarburg(Bz Trier)": null,
- "Saarhölzbach": null,
- "Saarlouis Hbf": null,
- "Saarmund": null,
- "Saasen": null,
- "Saatel": null,
- "Sachsen(b Ansbach)": null,
- "Sachsendorf(Calbe)": null,
- "Sachsenhausen(Nordb)": null,
- "Sachsenheim": null,
- "Sagard": null,
- "Sagehorn": null,
- "Salach": null,
- "Salem": null,
- "Sallach": null,
- "Salmtal": null,
- "Salzbergen": null,
- "Salzgitter-Bad": null,
- "Salzgitter-Immendorf": null,
- "Salzgitter-Lebenstedt": null,
- "Salzgitter-Ringelheim": null,
- "Salzgitter-Thiede": null,
- "Salzgitter-Watenstedt": null,
- "Salzkotten": null,
- "Salzwedel": null,
- "Samtens": null,
- "Sand(Niederbay)": null,
- "Sande": null,
- "Sandebeck": null,
- "Sanderbusch": null,
- "Sandersleben(Anh)": null,
- "Sandförde": null,
- "Sandhagen(b Bad Dob)": null,
- "Sandkrug": null,
- "Sandwehle": null,
- "Sangerhausen": null,
- "Sanitz(b Rostock)": null,
- "Sanssouci": null,
- "Sarnau": null,
- "Sarnow": null,
- "Sarstedt": null,
- "Sasbach am Kaiserstuhl": null,
- "Sassenroth": null,
- "Sassnitz": null,
- "Satteldorf": null,
- "Satzvey": null,
- "Sauerlach": null,
- "Saulgrub": null,
- "Saulheim": null,
- "Schafbrücke": null,
- "Schaftlach": null,
- "Schaidt(Pfalz)": null,
- "Schalchen": null,
- "Schalkau": null,
- "Schalkau Mitte": null,
- "Schalksmühle": null,
- "Schalkstetten": null,
- "Schallstadt": null,
- "Schameder": null,
- "Schandelah": null,
- "Scharbeutz": null,
- "Scharfenstein": null,
- "Scharmede": null,
- "Scharstorf": null,
- "Schechen": null,
- "Scheeßel": null,
- "Scheidt(Saar)": null,
- "Schelklingen": null,
- "Schemmerberg": null,
- "Schenkenzell": null,
- "Scheppach": null,
- "Scherfede": null,
- "Scheuerfeld(Sieg)": null,
- "Scheven": null,
- "Schieder": null,
- "Schierbrok": null,
- "Schierke": null,
- "Schierling": null,
- "Schierstedt": null,
- "Schifferstadt": null,
- "Schifferstadt Süd": null,
- "Schiffweiler": null,
- "Schiltach": null,
- "Schiltach Mitte": null,
- "Schimborn": null,
- "Schirgiswalde-Kirschau": null,
- "Schirnding": null,
- "Schkeuditz": null,
- "Schkeuditz West": null,
- "Schkopau": null,
- "Schladen(Harz)": null,
- "Schladern(Sieg)": null,
- "Schlatt(Hohenz)": null,
- "Schlechtbach": null,
- "Schleife": null,
- "Schleswig": null,
- "Schliengen": null,
- "Schlierbach(Schwalm-Eder-Kr.)": null,
- "Schliersee": null,
- "Schloß Holte": null,
- "Schluchsee": null,
- "Schlüchtern": null,
- "Schmachtenhagen": null,
- "Schmalkalden": null,
- "Schmalkalden-Fachhochschule": null,
- "Schmalnau": null,
- "Schmidtheim": null,
- "Schmiechen": null,
- "Schmiechen Albbahn": null,
- "Schmiechen(Schwab)": null,
- "Schmilka-Hirschmühle": null,
- "Schmollensee": null,
- "Schmölln(Ol)": null,
- "Schmölln(Thür)": null,
- "Schnabelwaid": null,
- "Schnaittach Markt": null,
- "Schneeberg im Odenwald": null,
- "Schneeberg(Mark)": null,
- "Schnega": null,
- "Schneidhain": null,
- "Schnelldorf": null,
- "Schneverdingen": null,
- "Schney": null,
- "Schnitzmühle": null,
- "Schoden-Ockfen": null,
- "Schondorf(Bay)": null,
- "Schongau": null,
- "Schonungen": null,
- "Schopfheim": null,
- "Schopfheim West": null,
- "Schopfloch(b Freudenstadt)": null,
- "Schopp": null,
- "Schorndorf": null,
- "Schorndorf-Hammerschlag": null,
- "Schortens-Heidmühle": null,
- "Schrezheim": null,
- "Schrobenhausen": null,
- "Schrozberg": null,
- "Schutzbach": null,
- "Schwaan": null,
- "Schwabach": null,
- "Schwabach-Limbach": null,
- "Schwabhausen(b Dachau)": null,
- "Schwabmünchen": null,
- "Schwabsberg": null,
- "Schwaig": null,
- "Schwaigern Ost": null,
- "Schwaigern(Württ)": null,
- "Schwaigern(Württ) West": null,
- "Schwaikheim": null,
- "Schwalbach(Taunus)Limes": null,
- "Schwalbach(Taunus)Nord": null,
- "Schwallungen": null,
- "Schwalmstadt-Wiera": null,
- "Schwandorf": null,
- "Schwanheide": null,
- "Schwante": null,
- "Schwarmstedt": null,
- "Schwarzburg": null,
- "Schwarzenbach(Saale)": null,
- "Schwarzenbach(b Pressath)": null,
- "Schwarzenbek": null,
- "Schwarzenberg": null,
- "Schwarzenberg Hp": null,
- "Schwarzenberg(Erzg)": null,
- "Schwarzenberg-Neuwelt": null,
- "Schwarzenfeld(Opf)": null,
- "Schwarzheide Ost": null,
- "Schwarzkollm": null,
- "Schwedt(Oder)": null,
- "Schwedt(Oder)Mitte": null,
- "Schweich(DB)": null,
- "Schweighofen": null,
- "Schweikershain": null,
- "Schweinfurt Hbf": null,
- "Schweinfurt Mitte": null,
- "Schweinfurt Stadt": null,
- "Schweinsburg-Culten": null,
- "Schweinsdorf": null,
- "Schwelm": null,
- "Schwelm West": null,
- "Schwenningen(Bay)": null,
- "Schwenningen(Neckar)": null,
- "Schwenzin": null,
- "Schweppenburg-Heilbrunnen": null,
- "Schwerin Hbf": null,
- "Schwerin Mitte": null,
- "Schwerin Süd": null,
- "Schwerin-Görries": null,
- "Schwerin-Görries": null,
- "Schwerin-Lankow": null,
- "Schwerin-Margaretenhof": null,
- "Schwerin-Warnitz": null,
- "Schwerin-Wüstmark": null,
- "Schwerte(Ruhr)": null,
- "Schweta Bf": null,
- "Schweta Gasth": null,
- "Schwetzingen": null,
- "Schwieberdingen": null,
- "Schwindegg": null,
- "Schwäbisch Gmünd": null,
- "Schwäbisch Hall": null,
- "Schwäbisch Hall-Hessental": null,
- "Schwörstadt": null,
- "Schöllkrippen": null,
- "Schömberg Stausee": null,
- "Schömberg(b Balingen)": null,
- "Schöna": null,
- "Schönau(Hörsel)": null,
- "Schönberg(Holstein)": null,
- "Schönberg(Meckl)": null,
- "Schönberg(Vogtl)": null,
- "Schönberger Strand": null,
- "Schönborn(Doberl)": null,
- "Schönebeck Süd": null,
- "Schönebeck(Elbe)": null,
- "Schönebeck-Bad Salzelmen": null,
- "Schönebeck-Felgeleben": null,
- "Schönebeck-Frohse": null,
- "Schöneck(Vogtl)": null,
- "Schöneck(Vogtl) Ferienpark": null,
- "Schöneck-Büdesheim": null,
- "Schöneck-Kilianstädten": null,
- "Schöneck-Oberdorfelden": null,
- "Schönerlinde": null,
- "Schönewörde": null,
- "Schönfließ(Mark) Dorf": null,
- "Schönfließ(b Oranienburg)": null,
- "Schöngeising": null,
- "Schönhausen(Elbe)": null,
- "Schönkirchen Bf": null,
- "Schönmünzach": null,
- "Schönow(Angerm)": null,
- "Schönstedt": null,
- "Schönwald(Oberfr)": null,
- "Schönwalde(Barnim)": null,
- "Schönwalde(Spreewald)": null,
- "Schöppenstedt": null,
- "Schötmar": null,
- "Schülldorf": null,
- "Schüptitz": null,
- "Schüttorf": null,
- "Sebnitz(Sachs)": null,
- "Sechshelden": null,
- "Sechtem": null,
- "Seckach": null,
- "Seddin": null,
- "Sedlitz Ost": null,
- "Seebach(Mühlhausen)": null,
- "Seebad Ahlbeck": null,
- "Seebad Heringsdorf": null,
- "Seebergen": null,
- "Seebrugg": null,
- "Seefeld(Mark)": null,
- "Seefeld-Hechendorf": null,
- "Seeg": null,
- "Seegefeld": null,
- "Seehausen(Altm)": null,
- "Seehausen(Uckermark)": null,
- "Seeleiten-Berggeist": null,
- "Seelow(Mark)": null,
- "Seelow-Gusow": null,
- "Seelvitz": null,
- "Seelze": null,
- "Seerhausen": null,
- "Seesen": null,
- "Seeshaupt": null,
- "Sehlem(Kr Wittlich)": null,
- "Sehma": null,
- "Sehnde": null,
- "Seiboldsdorf": null,
- "Seifersdorf": null,
- "Seifhennersdorf": null,
- "Seitschen": null,
- "Selb Nord": null,
- "Selb Stadt": null,
- "Selb-Plößberg": null,
- "Selbitz": null,
- "Selhausen": null,
- "Seligenstadt Mainschleifenbahn": null,
- "Seligenstadt(Hess)": null,
- "Seligenstadt(b Würzburg)": null,
- "Sellin(Rügen) Ost": null,
- "Sellin(Rügen) West": null,
- "Sellstedt": null,
- "Selm": null,
- "Selm-Beifang": null,
- "Seltendorf": null,
- "Senden": null,
- "Senftenberg": null,
- "Sennelager": null,
- "Sennestadt": null,
- "Sennfeld": null,
- "Serams": null,
- "Serrig": null,
- "Sersheim": null,
- "Seubersdorf": null,
- "Seulberg": null,
- "Seulbitz": null,
- "Seybothenreuth": null,
- "Siebeldingen-Birkweiler": null,
- "Siedlinghausen": null,
- "Siegburg Bahnhof": null,
- "Siegburg/Bonn": null,
- "Siegelsbach": null,
- "Siegelsdorf": null,
- "Siegen": null,
- "Siegen-Geisweid": null,
- "Siegen-Weidenau": null,
- "Siegsdorf": null,
- "Sierksdorf": null,
- "Siersburg": null,
- "Siershahn": null,
- "Siglingen": null,
- "Sigmaringen": null,
- "Sigmaringendorf": null,
- "Sigmaringendorf": null,
- "Silbach": null,
- "Silberhausen": null,
- "Silberhütte NE": null,
- "Silberstraße": null,
- "Simbach(Inn)": null,
- "Simmelsdorf-Hüttenbach": null,
- "Simtshausen": null,
- "Sindelfingen": null,
- "Sindorf": null,
- "Sindorf": null,
- "Singen Industriegebiet": null,
- "Singen Landesgartenschau": null,
- "Singen(Hohentwiel)": null,
- "Singen(Thür)": null,
- "Singlis": null,
- "Sinn": null,
- "Sinsheim Museum/Arena": null,
- "Sinsheim(Elsenz) Hbf": null,
- "Sinzheim": null,
- "Sinzheim Nord": null,
- "Sinzig(Rhein)": null,
- "Sinzing": null,
- "Sipplingen": null,
- "Sitzendorf-Unterweißbach": null,
- "Soest": null,
- "Sohl": null,
- "Sohland": null,
- "Solingen Grünewald": null,
- "Solingen Hbf": null,
- "Solingen Mitte": null,
- "Solingen Vogelpark": null,
- "Solingen-Schaberg": null,
- "Sollstedt": null,
- "Solms": null,
- "Solnhofen": null,
- "Solpke": null,
- "Soltau Nord": null,
- "Soltau(Han)": null,
- "Soltendieck": null,
- "Sondern": null,
- "Sondernach": null,
- "Sondernheim": null,
- "Sondershausen": null,
- "Sonneberg(Thür)Hbf": null,
- "Sonneberg(Thür)Nord": null,
- "Sonneberg(Thür)Ost": null,
- "Sonneberg(Thür)West": null,
- "Sontheim(Schwab)": null,
- "Sontheim-Brenz": null,
- "Sonthofen": null,
- "Sontra": null,
- "Sophienhof": null,
- "Sorge": null,
- "Sottrum": null,
- "Soyen": null,
- "Spaichingen": null,
- "Spaichingen Mitte": null,
- "Sparrieshoop": null,
- "Spay": null,
- "Spechtritz": null,
- "Speele": null,
- "Speicher": null,
- "Speikern": null,
- "Speyer Hbf": null,
- "Speyer Nord-West": null,
- "Spich": null,
- "Spiegelau": null,
- "Spielberg": null,
- "Sponholz": null,
- "Spornitz": null,
- "Spremberg": null,
- "Sprendlingen(Rheinhess)": null,
- "Sprendlingen(Rheinhess)": null,
- "Sprendlingen(Rheinhess)": null,
- "Springe": null,
- "Sprötze": null,
- "St Alban": null,
- "St Augustin Markt": null,
- "St Egidien": null,
- "St Georgen(Schwarzw)": null,
- "St Goar": null,
- "St Goarshausen": null,
- "St Ilgen-Sandhausen": null,
- "St Ingbert": null,
- "St Koloman": null,
- "St Mang": null,
- "St Michaelisdonn": null,
- "St Ottilien": null,
- "St Thomas": null,
- "St Wendel": null,
- "Stade": null,
- "Stadt Wehlen(Sachs)": null,
- "Stadtallendorf": null,
- "Stadthagen": null,
- "Stadtilm": null,
- "Stadtoldendorf": null,
- "Stadtprozelten": null,
- "Stadtroda": null,
- "Staffel": null,
- "Stahringen": null,
- "Stambach": null,
- "Stammbach": null,
- "Stapelburg": null,
- "Starnberg": null,
- "Starnberg Nord": null,
- "Stauchitz": null,
- "Staudernheim": null,
- "Staufen": null,
- "Staufen Süd": null,
- "Staßfurt": null,
- "Stederdorf(Kr Uelzen)": null,
- "Stegenwaldhaus": null,
- "Steilküste/Wittenbeck": null,
- "Stein(Traun)": null,
- "Steina": null,
- "Steinach(Baden)": null,
- "Steinach(Thür)": null,
- "Steinach(Thür)Süd": null,
- "Steinach(b Rothenburg ob der Tauber)": null,
- "Steinalben": null,
- "Steinau(Straße)": null,
- "Steinbach am Wald": null,
- "Steinbach-Hallenberg": null,
- "Steinebach": null,
- "Steinefrenz": null,
- "Steinen": null,
- "Steinerne Renne": null,
- "Steinfeld(Oldb)": null,
- "Steinfeld(Pfalz)": null,
- "Steinfeld(Stendal)": null,
- "Steinfurt-Borghorst": null,
- "Steinfurt-Burgsteinfurt": null,
- "Steinfurt-Grottenkamp": null,
- "Steinhagen(Westf)": null,
- "Steinhagen(Westf) Bielef. Str.": null,
- "Steinhausen-Neuburg": null,
- "Steinheim(Main)": null,
- "Steinheim(Westf)": null,
- "Steinhöring": null,
- "Steinpleis": null,
- "Steinsfurt": null,
- "Steinweiler": null,
- "Steinwenden": null,
- "Steinwiesen Bf": null,
- "Stelle": null,
- "Stendal": null,
- "Stendal Vorbf": null,
- "Stendal-Stadtsee": null,
- "Stenn": null,
- "Sterbfritz": null,
- "Sternfeld": null,
- "Sternhaus-Haferfeld": null,
- "Sternhaus-Ramberg": null,
- "Sterzhausen": null,
- "Stetten (b. Haigerloch)": null,
- "Stetten am Heuchelberg": null,
- "Stetten(Donau)": null,
- "Stetten(Schwab)": null,
- "Stetten-Beinstein": null,
- "Stettfeld(Baden)": null,
- "Stiege": null,
- "Stift Keppel-Allenbach": null,
- "Stockach NE": null,
- "Stockau": null,
- "Stockdorf": null,
- "Stockhausen(Lahn)": null,
- "Stockheim(Oberfr)": null,
- "Stockheim(Unterfr)": null,
- "Stockstadt(Main)": null,
- "Stockstadt(Rhein)": null,
- "Stolberg(Rheinl)Gbf": null,
- "Stolberg(Rheinl)Hbf": null,
- "Stolberg(Rheinl)Hbf": null,
- "Stolberg(Rheinl)Hbf Gl.27": null,
- "Stolberg(Rheinl)Hbf Gl.44": null,
- "Stolberg-Altstadt": null,
- "Stolberg-Mühlener Bahnhof": null,
- "Stolberg-Rathaus": null,
- "Stolberg-Schneidmühle": null,
- "Stollberg Schlachthofstraße": null,
- "Stollberg(Sachs)": null,
- "Stolpen": null,
- "Stommeln": null,
- "Storkow(Mark)": null,
- "Storzingen": null,
- "Stotternheim": null,
- "Stralsund Hbf": null,
- "Stralsund Rügendamm": null,
- "Stralsund-Grünhufe": null,
- "Strasburg(Uckerm)": null,
- "Straubing": null,
- "Straubing-Ost": null,
- "Strausberg": null,
- "Strausberg Nord": null,
- "Strausberg Stadt": null,
- "Strausberg-Hegermühle": null,
- "Straußfurt": null,
- "Straß-Moos": null,
- "Straßberg(Harz)": null,
- "Straßberg-Glasebach": null,
- "Straßberg-Winterlingen": null,
- "Straßkirchen": null,
- "Strohkirchen": null,
- "Strullendorf": null,
- "Struthütten": null,
- "Stryck": null,
- "Stubben": null,
- "Stubbenfelde": null,
- "Stubersheim": null,
- "Stumsdorf": null,
- "Stuttgart Ebitzweg": null,
- "Stuttgart Feuersee": null,
- "Stuttgart Flughafen/Messe": null,
- "Stuttgart Hbf": null,
- "Stuttgart Hbf (tief)": null,
- "Stuttgart Neckarpark": null,
- "Stuttgart Nord": null,
- "Stuttgart Nord": null,
- "Stuttgart Nürnberger Str.": null,
- "Stuttgart Schwabstr.": null,
- "Stuttgart Stadtmitte": null,
- "Stuttgart Universität": null,
- "Stuttgart-Bad Cannstatt": null,
- "Stuttgart-Feuerbach": null,
- "Stuttgart-Feuerbach": null,
- "Stuttgart-Münster": null,
- "Stuttgart-Obertürkheim": null,
- "Stuttgart-Rohr": null,
- "Stuttgart-Sommerrain": null,
- "Stuttgart-Untertürkheim": null,
- "Stuttgart-Untertürkheim": null,
- "Stuttgart-Untertürkheim": null,
- "Stuttgart-Vaihingen": null,
- "Stuttgart-Zazenhausen": null,
- "Stuttgart-Zuffenhausen": null,
- "Stuttgart-Österfeld": null,
- "Stühlingen": null,
- "Stützerbach": null,
- "Subzin-Liessow": null,
- "Suchsdorf": null,
- "Suderburg": null,
- "Suerhop": null,
- "Suhl": null,
- "Suhl-Heinrichs": null,
- "Sukow(b Schwerin)": null,
- "Sulmingen": null,
- "Sulz(Neckar)": null,
- "Sulzbach(Inn)": null,
- "Sulzbach(Main)": null,
- "Sulzbach(Murr)": null,
- "Sulzbach(Saar)": null,
- "Sulzbach(Saar)Altenwald": null,
- "Sulzbach(Taunus)": null,
- "Sulzbach(Taunus)Nord": null,
- "Sulzbach-Rosenberg": null,
- "Sulzbach-Rosenberg Hütte": null,
- "Sulzberg": null,
- "Sulzfeld(Baden)": null,
- "Swisttal-Odendorf": null,
- "Syke": null,
- "Sylbach": null,
- "Syrau": null,
- "Sythen": null,
- "Sättelstädt": null,
- "Söllichau": null,
- "Söllingen Kapellenstraße": null,
- "Söllingen Reetzstr.": null,
- "Söllingen(b Karlsr)": null,
- "Söllingen(b Karlsr)": null,
- "Sömmerda": null,
- "Sörup": null,
- "Süderbrarup": null,
- "Süderdeich": null,
- "Süderlügum": null,
- "Sülstorf": null,
- "Sülzbach": null,
- "Sülzbach Schule": null,
- "Sülzenbrücken": null,
- "Sünching": null,
- "Süßen": null,
- "Taben": null,
- "Tacherting": null,
- "Tamm(Württ)": null,
- "Tangerhütte": null,
- "Tangermünde": null,
- "Tangermünde West": null,
- "Tanndorf": null,
- "Tanneneck": null,
- "Tannheim(Württ)": null,
- "Tannroda": null,
- "Tantow": null,
- "Tapfheim": null,
- "Tarp": null,
- "Tating": null,
- "Taubenheim(Spree)": null,
- "Tauberbischofsheim": null,
- "Tauberfeld": null,
- "Taucha(Leipzig)": null,
- "Taufkirchen": null,
- "Tautenhain": null,
- "Tegernsee": null,
- "Teicha": null,
- "Teichland": null,
- "Teisendorf": null,
- "Teisnach": null,
- "Teisnach Rohde&Schwarz": null,
- "Telgte": null,
- "Teltow": null,
- "Teltow Stadt": null,
- "Temmels": null,
- "Templin": null,
- "Templin Stadt": null,
- "Teningen-Mundingen": null,
- "Teschenhagen": null,
- "Teschow": null,
- "Tessin": null,
- "Tessin West": null,
- "Teterow": null,
- "Teuchern": null,
- "Teufelsmühle": null,
- "Teutschenthal": null,
- "Teutschenthal Ost": null,
- "Thale Hbf": null,
- "Thale Musestieg": null,
- "Thaleischweiler-Fröschen": null,
- "Thalfingen(b Ulm)": null,
- "Thalheim (b Oschatz)": null,
- "Thalheim(Erzgeb)": null,
- "Thann-Matzbach": null,
- "Thansüß": null,
- "Tharandt": null,
- "Thayngen": null,
- "Theisbergstegen": null,
- "Theißen": null,
- "Themar": null,
- "Thermalbad Wiesenbad": null,
- "Thesdorf": null,
- "Thiergarten(Hohenz)": null,
- "Thyrow": null,
- "Thüngersheim": null,
- "Thür": null,
- "Tiebensee": null,
- "Tiefenau": null,
- "Tiefenbach(b Passau)": null,
- "Tiefenbachmühle": null,
- "Tiengen(Hochrhein)": null,
- "Timmendorferstrand": null,
- "Titisee": null,
- "Tittmoning-Wiesmühl": null,
- "Torgau": null,
- "Torgelow": null,
- "Tornesch": null,
- "Tostedt": null,
- "Traben-Trarbach": null,
- "Trabitz": null,
- "Trais-Horloff": null,
- "Trasadingen": null,
- "Trassenheide": null,
- "Trassenmoor": null,
- "Traundorf": null,
- "Traunreut": null,
- "Traunstein": null,
- "Traunstein Klinikum": null,
- "Trebbin": null,
- "Treben-Lehma": null,
- "Trebgast": null,
- "Trebitz(Elbe)": null,
- "Trebitz(Könnern)": null,
- "Trebnitz(Mark)": null,
- "Trechtingshausen": null,
- "Treis-Karden": null,
- "Treuchtlingen": null,
- "Treuen": null,
- "Treuenbrietzen": null,
- "Treuenbrietzen Süd": null,
- "Treysa": null,
- "Triangel": null,
- "Triberg": null,
- "Triebes": null,
- "Triefenried": null,
- "Trier Hbf": null,
- "Trier Süd": null,
- "Triesdorf": null,
- "Triptis": null,
- "Trochtelfingen ALB-GOLD": null,
- "Trochtelfingen(Hohenz)": null,
- "Trochtelfingen(b Bopfingen)": null,
- "Troisdorf": null,
- "Trompet": null,
- "Tromsdorf": null,
- "Trossingen Bahnhof": null,
- "Trossingen Bahnhof": null,
- "Trossingen Stadt": null,
- "Trostberg": null,
- "Tulling": null,
- "Tuttlingen": null,
- "Tuttlingen Gänsäcker": null,
- "Tuttlingen Nord": null,
- "Tuttlingen Schulen": null,
- "Tuttlingen Zentrum": null,
- "Tutzing": null,
- "Twiste": null,
- "Twistringen": null,
- "Töging(Inn)": null,
- "Tönning": null,
- "Töppeln": null,
- "Tübingen Hbf": null,
- "Tübingen West": null,
- "Tübingen-Derendingen": null,
- "Tübingen-Lustnau": null,
- "Türkenfeld": null,
- "Türkheim(Bay)Bf": null,
- "Türkismühle": null,
- "Tüßling": null,
- "Ubbedissen": null,
- "Ubstadt Ort": null,
- "Ubstadt Salzbrunnenstr": null,
- "Ubstadt Uhlandstr.": null,
- "Ubstadt-Weiher": null,
- "Uchtspringe": null,
- "Uder": null,
- "Uebigau": null,
- "Ueckermünde": null,
- "Ueckermünde Stadthafen": null,
- "Uelzen": null,
- "Uffenheim": null,
- "Uffing a Staffelsee": null,
- "Uhingen": null,
- "Uhldingen-Mühlhofen": null,
- "Uhlerborn": null,
- "Uhlstädt": null,
- "Uhsmannsdorf": null,
- "Ulbersdorf": null,
- "Ulm Hbf": null,
- "Ulm Ost": null,
- "Ulm-Donautal": null,
- "Ulm-Söflingen": null,
- "Ulzburg Süd": null,
- "Umrathshausen Bf": null,
- "Umrathshausen Ort": null,
- "Unadingen": null,
- "Undorf": null,
- "Unfriedsdorf": null,
- "Ungedanken": null,
- "Unkel": null,
- "Unna": null,
- "Unna West": null,
- "Unna-Königsborn": null,
- "Unnau-Korb": null,
- "Unterammergau": null,
- "Unterasbach": null,
- "Unteraschau": null,
- "Unterelchingen": null,
- "Unterföhring": null,
- "Untergimpern": null,
- "Untergrainau": null,
- "Untergriesheim": null,
- "Untergrombach": null,
- "Unterhaching": null,
- "Unterharmersbach": null,
- "Unterhausen(Bay)": null,
- "Unterheckenhofen": null,
- "Unterjesingen Mitte": null,
- "Unterjesingen Sandäcker": null,
- "Unterkochen": null,
- "Unterlemnitz": null,
- "Unterlenningen": null,
- "Unterloquitz": null,
- "Unterlüß": null,
- "Untermaubach-Schlagstein": null,
- "Untermaßfeld": null,
- "Unterneudorf": null,
- "Unterreichenbach": null,
- "Unterschleißheim": null,
- "Untersteinach(Bayr)": null,
- "Untersteinach(b Stadtsteinach)": null,
- "Untersulzbach": null,
- "Unterwellenborn": null,
- "Unterwiesenthal": null,
- "Unteröwisheim Bf": null,
- "Unteröwisheim M.-Luther-Str.": null,
- "Uphusum": null,
- "Urbach(b Schorndorf)": null,
- "Urft": null,
- "Urmersbach": null,
- "Urmitz": null,
- "Urmitz Rheinbrücke": null,
- "Urschalling": null,
- "Urspring": null,
- "Usch-Zendscheid": null,
- "Usingen": null,
- "Uslar": null,
- "Usseln": null,
- "Utting": null,
- "Utzedel": null,
- "Vach": null,
- "Vachdorf": null,
- "Vachendorf": null,
- "Vahldorf": null,
- "Vaihingen(Enz)": null,
- "Vallendar": null,
- "Varel(Oldb)": null,
- "Vastorf": null,
- "Vaterstetten": null,
- "Vechelde": null,
- "Vechta": null,
- "Vechta-Stoppelmarkt": null,
- "Vehlefanz": null,
- "Veilsdorf": null,
- "Veitshöchheim": null,
- "Velbert Rosenhügel": null,
- "Velbert-Langenberg": null,
- "Velbert-Neviges": null,
- "Velbert-Nierenhof": null,
- "Velden(b Hersbruck)": null,
- "Velgast": null,
- "Vellmar-Niedervellmar": null,
- "Vellmar-Obervellmar": null,
- "Vellmar-Osterberg/EKZ": null,
- "Velten(Mark)": null,
- "Ventschow": null,
- "Verden(Aller)": null,
- "Veringendorf": null,
- "Veringenstadt": null,
- "Vernawahlshausen": null,
- "Vetschau": null,
- "Vettweiß": null,
- "Viechtach": null,
- "Vienenburg": null,
- "Vierenstraße": null,
- "Vierkirchen-Esterhofen": null,
- "Viernau": null,
- "Viersen": null,
- "Vieselbach": null,
- "Villingen(Schwarzw)": null,
- "Villingen-Schwenningen Eisstadion": null,
- "Villingen-Schwenningen Hammerstatt": null,
- "Villmar": null,
- "Vilsbiburg": null,
- "Vilseck": null,
- "Vilshofen(Niederbay)": null,
- "Vinzelberg": null,
- "Visselhövede": null,
- "Vlotho": null,
- "Voerde(Niederrhein)": null,
- "Vogelsang(Gransee)": null,
- "Vogelweh": null,
- "Vohburg": null,
- "Voigtsgrün": null,
- "Voigtstedt": null,
- "Voldagsen": null,
- "Volkach-Astheim": null,
- "Volkmarsen": null,
- "Volkringhausen": null,
- "Volpriehausen": null,
- "Vorhop": null,
- "Vormwald": null,
- "Vormwald Dorf": null,
- "Vorra(Pegnitz)": null,
- "Voßloch": null,
- "Vöhl-Ederbringhausen": null,
- "Vöhl-Herzhausen": null,
- "Vöhl-Schmittlotheim": null,
- "Vöhl-Thalitter": null,
- "Vöhringen": null,
- "Vöhrum": null,
- "Völklingen": null,
- "Völksen/Eldagsen": null,
- "Wabern(Bz Kassel)": null,
- "Wachenheim(Pfalz)": null,
- "Wachenheim-Mölsheim": null,
- "Wackershofen": null,
- "Waffenbrunn": null,
- "Waggonfabrik": null,
- "Waghäusel": null,
- "Waging": null,
- "Wahlbach(Kr Siegen)": null,
- "Wahlheim": null,
- "Wahlitz": null,
- "Wahlstedt": null,
- "Wahlwies": null,
- "Wahrenholz": null,
- "Waiblingen": null,
- "Waibstadt": null,
- "Waigolshausen": null,
- "Wakendorf": null,
- "Walddrehna": null,
- "Waldenburg(Württ)": null,
- "Waldershof": null,
- "Waldfischbach": null,
- "Waldhausen(b Geislingen)": null,
- "Waldhausen(b Schorndorf)": null,
- "Waldheim": null,
- "Waldkirch": null,
- "Waldkirchen(Erzgeb)": null,
- "Waldkirchen(Niederbay.)": null,
- "Waldkraiburg": null,
- "Waldmünchen": null,
- "Waldshut": null,
- "Waldshut": null,
- "Walhausen(Saar)": null,
- "Walheim(Württ)": null,
- "Walkenried": null,
- "Wallau(Lahn)": null,
- "Walldorf(Hess)": null,
- "Walldorf(Werra)": null,
- "Walldürn": null,
- "Wallersdorf": null,
- "Wallertheim": null,
- "Walleshausen": null,
- "Wallhausen(Helme)": null,
- "Wallhausen(Württ)": null,
- "Wallwitz(Saalkr)": null,
- "Walpertskirchen": null,
- "Walporzheim": null,
- "Walschleben": null,
- "Walsleben": null,
- "Walsrode": null,
- "Waltershausen": null,
- "Waltershausen Schnepfenthal": null,
- "Waltershausen Schnepfenthal": null,
- "Wandersleben": null,
- "Wandlitz": null,
- "Wandlitzsee": null,
- "Wangen(Allgäu)": null,
- "Wangen(Unstrut)": null,
- "Wanne-Eickel Hbf": null,
- "Wannweil": null,
- "Wansleben am See": null,
- "Warburg(Westf)": null,
- "Waren(Müritz)": null,
- "Warendorf": null,
- "Warendorf-Einen-Müssingen": null,
- "Warenshof": null,
- "Warmbad": null,
- "Warnemünde": null,
- "Warnemünde Werft": null,
- "Warngau": null,
- "Warnitz(Uckermark)": null,
- "Warthausen": null,
- "Warthausen": null,
- "Wasbek": null,
- "Wasenweiler": null,
- "Wasseralfingen": null,
- "Wasserburg(Bodensee)": null,
- "Wasserburg(Günz)": null,
- "Wasserburg(Inn)Bf": null,
- "Wasserliesch": null,
- "Wasserthaleben": null,
- "Wassertrüdingen": null,
- "Wasserzell(b Eichstätt)": null,
- "Wasungen": null,
- "Wattenscheid": null,
- "Wattenscheid-Höntrop": null,
- "Watzelsteg": null,
- "Watzenborn-Steinberg": null,
- "Weckesheim": null,
- "Weddel(Braunschw)": null,
- "Wedel(Holst)": null,
- "Weener": null,
- "Weesenstein": null,
- "Weetzen": null,
- "Weeze": null,
- "Wefensleben": null,
- "Wega": null,
- "Wegberg": null,
- "Wegeleben": null,
- "Wegenstedt": null,
- "Wehdel": null,
- "Wehr(Mosel)": null,
- "Wehr-Brennet": null,
- "Wehrden": null,
- "Wehretal-Reichensachsen": null,
- "Wehrheim": null,
- "Weibhausen": null,
- "Weichering": null,
- "Weickersdorf(Sachs)": null,
- "Weickersgrüben": null,
- "Weida": null,
- "Weida Altstadt": null,
- "Weida Mitte": null,
- "Weiden(Oberpf)": null,
- "Weidenbach": null,
- "Weidenberg": null,
- "Weidenthal": null,
- "Weiding": null,
- "Weiherhammer": null,
- "Weiherhof": null,
- "Weikersheim": null,
- "Weil am Rhein": null,
- "Weil am Rhein Ost": null,
- "Weil am Rhein-Gartenstadt": null,
- "Weil am Rhein-Pfädlistraße": null,
- "Weil der Stadt": null,
- "Weil im Schönbuch Röte": null,
- "Weil im Schönbuch Troppel": null,
- "Weil im Schönbuch Untere Halde": null,
- "Weilbach(Unterallg)": null,
- "Weilbach(Unterfr)": null,
- "Weilburg": null,
- "Weiler (Brohltal)": null,
- "Weiler(Rems)": null,
- "Weilerswist": null,
- "Weilerswist-Derkum": null,
- "Weilheim(Oberbay)": null,
- "Weilheim(Württ)": null,
- "Weilimdorf": null,
- "Weimar": null,
- "Weimar Berkaer Bf": null,
- "Weimar West": null,
- "Weinböhla Hp": null,
- "Weingarten Berg": null,
- "Weingarten(Baden)": null,
- "Weinheim(Bergstr)": null,
- "Weinheim-Lützelsachsen": null,
- "Weinsberg": null,
- "Weinsberg West": null,
- "Weinsberg/Ellhofen Gewerbegebiet": null,
- "Weischlitz": null,
- "Weisen": null,
- "Weisenbach": null,
- "Weisenheim(Sand)": null,
- "Weiterstadt": null,
- "Weixdorf": null,
- "Weixdorf Bad": null,
- "Weizen": null,
- "Weizern-Hopferau": null,
- "Weißandt-Gölzau": null,
- "Weißenau": null,
- "Weißenburg(Bay)": null,
- "Weißenfels": null,
- "Weißenfels West": null,
- "Weißenhorn": null,
- "Weißenhorn-Eschach": null,
- "Weißenohe": null,
- "Weißenthurm": null,
- "Weißer See": null,
- "Weißes Roß": null,
- "Weißwasser(Oberlausitz)": null,
- "Welgesheim-Zotzenheim": null,
- "Welkers": null,
- "Wellen(Magdeburg)": null,
- "Wellen(Mosel)": null,
- "Wellendorf": null,
- "Wellmitz": null,
- "Welschen Ennest": null,
- "Welschingen-Neuhausen": null,
- "Welver": null,
- "Wemmetsweiler Rathaus": null,
- "Wendisch Evern": null,
- "Wendisch-Rietz": null,
- "Wendlingen(Neckar)": null,
- "Wennedach": null,
- "Wennigsen(Deister)": null,
- "Wensickendorf": null,
- "Werbig": null,
- "Werbig": null,
- "Werbig": null,
- "Werdau": null,
- "Werdau Nord": null,
- "Werder(Havel)": null,
- "Werdohl": null,
- "Werdorf": null,
- "Werl": null,
- "Wernau(Neckar)": null,
- "Wernberg": null,
- "Werne a d Lippe": null,
- "Werneuchen": null,
- "Wernfeld": null,
- "Wernigerode": null,
- "Wernigerode Elmowerk": null,
- "Wernigerode Hochschule Harz": null,
- "Wernigerode Westerntor": null,
- "Wernigerode-Hasserode": null,
- "Wernshausen": null,
- "Wertach-Haslach": null,
- "Wertheim": null,
- "Wertheim-Bestenheid": null,
- "Werther": null,
- "Wesel": null,
- "Wesel Feldmark": null,
- "Wesenberg": null,
- "Wesselburen": null,
- "Wesseln": null,
- "Westbarthausen": null,
- "Westbevern": null,
- "Westendorf": null,
- "Westerburg": null,
- "Westerham": null,
- "Westerhausen": null,
- "Westerland(Sylt)": null,
- "Westerland(Sylt), Sylt Shuttle": null,
- "Westerstede-Ocholt": null,
- "Westerstetten": null,
- "Westewitz-Hochweitzschen": null,
- "Westhausen": null,
- "Westheim(Schwab)": null,
- "Westheim(Westf)": null,
- "Westheim-Langendorf": null,
- "Westönnen": null,
- "Wetter(Hessen)": null,
- "Wetter(Ruhr)": null,
- "Wetterzeube": null,
- "Wetzlar": null,
- "Weßling(Oberbay)": null,
- "Wickede(Ruhr)": null,
- "Wicklesgreuth": null,
- "Wickrath": null,
- "Wiebelskirchen": null,
- "Wiemersdorf": null,
- "Wieren": null,
- "Wiesa(Erzgeb)": null,
- "Wiesau(Oberpf)": null,
- "Wiesbaden Hbf": null,
- "Wiesbaden Ost": null,
- "Wiesbaden-Biebrich": null,
- "Wiesbaden-Erbenheim": null,
- "Wiesbaden-Igstadt": null,
- "Wiesbaden-Schierstein": null,
- "Wiesenau": null,
- "Wiesenburg(Mark)": null,
- "Wiesenburg(Sachs)": null,
- "Wiesenfeld": null,
- "Wiesenfeld(b Coburg)": null,
- "Wiesental": null,
- "Wiesenthau": null,
- "Wieslensdorf": null,
- "Wiesloch-Walldorf": null,
- "Wiesmühl(Alz)": null,
- "Wiesthal": null,
- "Wilburgstetten Bf": null,
- "Wilchingen-Hallau": null,
- "Wildau": null,
- "Wildberg(Württ)": null,
- "Wildeck-Bosserode": null,
- "Wildeck-Hönebach": null,
- "Wildeck-Obersuhl": null,
- "Wildeshausen": null,
- "Wilferdingen-Singen": null,
- "Wilgartswiesen": null,
- "Wilhelmsdorf": null,
- "Wilhelmshaven Hbf": null,
- "Wilhelmshorst": null,
- "Wilhelmshütte(Lahn)": null,
- "Wilhermsdorf": null,
- "Wilhermsdorf Mitte": null,
- "Wilischthal": null,
- "Wilkau-Haßlau": null,
- "Willebadessen": null,
- "Willingen": null,
- "Willmenrod": null,
- "Willmering": null,
- "Willsbach": null,
- "Wilmersdorf(Angerm)": null,
- "Wilsenroth": null,
- "Wilster": null,
- "Wilthen": null,
- "Wiltingen(Saar)": null,
- "Wincheringen": null,
- "Windelsbleiche": null,
- "Winden(Pfalz)": null,
- "Windischeschenbach": null,
- "Windsbach": null,
- "Wingerode": null,
- "Wingst": null,
- "Winkelhaid": null,
- "Winnenden": null,
- "Winningen(Mosel)": null,
- "Winninghausen": null,
- "Winnweiler": null,
- "Winsen(Luhe)": null,
- "Winterbach(b Schorndorf)": null,
- "Winterberg(Westf)": null,
- "Winterhausen": null,
- "Wintermoor": null,
- "Wipperdorf": null,
- "Wirges": null,
- "Wirtheim": null,
- "Wismar": null,
- "Wissen(Sieg)": null,
- "Wissingen": null,
- "Wittbräucke": null,
- "Witten Hbf": null,
- "Witten-Annen Nord": null,
- "Wittenberge": null,
- "Wittenhagen": null,
- "Wittgensdorf Mitte": null,
- "Wittgensdorf ob Bf": null,
- "Wittighausen": null,
- "Wittingen": null,
- "Wittlich Hbf": null,
- "Wittlingen": null,
- "Wittmund": null,
- "Wittstock(Dosse)": null,
- "Witzenhausen Nord": null,
- "Witzighausen": null,
- "Witzschdorf": null,
- "Witzwort": null,
- "Woffleben": null,
- "Wohltorf": null,
- "Wolfach": null,
- "Wolfegg": null,
- "Wolfen(Bitterfeld)": null,
- "Wolfenbüttel": null,
- "Wolfenbüttel": null,
- "Wolferode": null,
- "Wolfgang(Kr Hanau)": null,
- "Wolfhagen": null,
- "Wolfratshausen": null,
- "Wolfsburg Hbf": null,
- "Wolfsgefärth": null,
- "Wolfsmünster": null,
- "Wolfstein": null,
- "Wolgast": null,
- "Wolgast Hafen": null,
- "Wolgaster Fähre": null,
- "Wolkenstein": null,
- "Wolkramshausen": null,
- "Wollbach(Baden)": null,
- "Wolmirstedt": null,
- "Wolterdingen(Han)": null,
- "Woltersdorf/Nuthe-Urstromtal": null,
- "Woltwiesche": null,
- "Worms Hbf": null,
- "Worpswede": null,
- "Wremen": null,
- "Wriezen": null,
- "Wrist": null,
- "Wulfen(Anh)": null,
- "Wulfen(Westf)": null,
- "Wulften": null,
- "Wullenstetten": null,
- "Wunsiedel-Holenbrunn": null,
- "Wunstorf": null,
- "Wuppertal Hbf": null,
- "Wuppertal-Barmen": null,
- "Wuppertal-Langerfeld": null,
- "Wuppertal-Oberbarmen": null,
- "Wuppertal-Ronsdorf": null,
- "Wuppertal-Sonnborn": null,
- "Wuppertal-Steinbeck": null,
- "Wuppertal-Unterbarmen": null,
- "Wuppertal-Vohwinkel": null,
- "Wuppertal-Zoologischer Garten": null,
- "Wurlitz": null,
- "Wurmlingen Mitte": null,
- "Wurmlingen Nord": null,
- "Wurzbach(Thür)": null,
- "Wurzen": null,
- "Wusterhausen(Dosse) NE": null,
- "Wustermark": null,
- "Wusterwitz": null,
- "Wustrau-Radensleben": null,
- "Wustweiler": null,
- "Wutha": null,
- "Wutike": null,
- "Wutöschingen": null,
- "Wyhlen": null,
- "Wächterhof": null,
- "Wächtersbach": null,
- "Wölfershausen": null,
- "Wölfersheim-Södel": null,
- "Wörlitz": null,
- "Wörnitzstein": null,
- "Wörrstadt": null,
- "Wörsdorf": null,
- "Wörth(Isar)": null,
- "Wörth(Main)": null,
- "Wörth(Rhein)": null,
- "Wörth(Rhein) Alte Bahnmeisterei": null,
- "Wörth(Rhein) Badallee": null,
- "Wörth(Rhein) Badepark": null,
- "Wörth(Rhein) Bienwaldhalle": null,
- "Wörth(Rhein) Bürgerpark": null,
- "Wörth(Rhein) Mozartstraße": null,
- "Wörth(Rhein) Rathaus": null,
- "Wörth(Rhein) Zügelstraße": null,
- "Wössingen": null,
- "Wössingen Ost": null,
- "Wülfrath-Aprath": null,
- "Wülknitz": null,
- "Wünschendorf": null,
- "Wünschendorf Nord": null,
- "Wünsdorf-Waldstadt": null,
- "Würgendorf": null,
- "Würgendorf (Ort)": null,
- "Würzbach(Saar)": null,
- "Würzburg Hbf": null,
- "Würzburg Süd": null,
- "Würzburg-Zell": null,
- "Wüstenbrand": null,
- "Wüstenfelde": null,
- "Wüstenselbitz": null,
- "Wüsting": null,
- "Xanten": null,
- "Zabeltitz": null,
- "Zachun": null,
- "Zahna": null,
- "Zaisenhausen": null,
- "Zapfendorf": null,
- "Zarrendorf": null,
- "Zeesen": null,
- "Zehdenick(Mark)": null,
- "Zehdenick-Neuhof": null,
- "Zeil": null,
- "Zeithain": null,
- "Zeitz": null,
- "Zell(Harmersbach)": null,
- "Zell(Wiesental)": null,
- "Zell-Romrod": null,
- "Zella-Mehlis": null,
- "Zella-Mehlis West": null,
- "Zellendorf": null,
- "Zellerthal": null,
- "Zempin": null,
- "Zennern": null,
- "Zepernick(Bernau)": null,
- "Zeppelinheim": null,
- "Zerbst/Anhalt": null,
- "Zerkall": null,
- "Zernsdorf": null,
- "Zerrenthin": null,
- "Zeulenroda unt Bf": null,
- "Zeutern Bf": null,
- "Zeutern Ost": null,
- "Zeutern Sportplatz": null,
- "Zeuthen": null,
- "Zeutsch": null,
- "Zielitz": null,
- "Zielitz Ort": null,
- "Zierenberg": null,
- "Zierenberg-Rosental": null,
- "Zieverich": null,
- "Zillendorf": null,
- "Ziltendorf": null,
- "Zimmern(Main-Tauber)": null,
- "Zimmern(b Seckach)": null,
- "Zimmersrode": null,
- "Zinnowitz": null,
- "Zirndorf": null,
- "Zirndorf Kneippallee": null,
- "Zirtow-Leussow": null,
- "Zittau": null,
- "Zittau Hp": null,
- "Zittau Süd": null,
- "Zittau Vorstadt": null,
- "Zoblitz": null,
- "Zollhaus(Villingen-Schwenningen)": null,
- "Zollhaus-Petersthal": null,
- "Zorneding": null,
- "Zossen": null,
- "Zotzenbach": null,
- "Zschaitz": null,
- "Zscherben": null,
- "Zschopau": null,
- "Zschopau Ost": null,
- "Zschortau": null,
- "Zusenhofen": null,
- "Zuzenhausen": null,
- "Zweibrücken Hbf": null,
- "Zwenkau-Großdalzig": null,
- "Zwickau Stadthalle": null,
- "Zwickau Zentrum": null,
- "Zwickau(Sachs)Hbf": null,
- "Zwickau-Pölbitz": null,
- "Zwickau-Schedewitz": null,
- "Zwiesel(Bay)": null,
- "Zwieselau": null,
- "Zwingenberg(Baden)": null,
- "Zwingenberg(Bergstr)": null,
- "Zwota": null,
- "Zwota-Zechenbach": null,
- "Zwotental": null,
- "Zwönitz": null,
- "Zöberitz": null,
- "Zörnigall": null,
- "Zühlsdorf": null,
- "Zülpich": null,
- "Züssow": null,
- "Züttlingen": null,
- "Äpfingen": null,
- "Öhringen Hbf": null,
- "Öhringen West": null,
- "Öhringen-Cappel": null,
- "Ölbronn-Dürrn": null,
- "Ötigheim": null,
- "Ötisheim": null,
- "Übach-Palenberg": null,
- "Überlingen": null,
- "Überlingen Therme": null,
- "Überlingen-Nußdorf": null,
- "Übersee": null,
- "Ückeritz": null,
- "Üdingen": null,
- "Ürzig(DB)": null,
- "Pogeez": null,
- "Klitten": null,
- "Lohsa": null,
- "Mücka": null,
- "Niesky": null,
- "Uhyst": null,
- "Chemnitz Küchwald": null,
- "Muldenberg": null,
- "Wengern Ost": null,
- "Wallenrod": null,
- "Alfhausen": null,
- "Wangerooge": null,
- "Barby": null,
- "Groß Behnitz": null,
- "Haynsburg": null,
- "Webau": null,
- "Niederarnbach": null,
- "Leutershausen-Wiedersbach": null,
- "Auersmacher": null,
- "Brebach": null,
- "Hanweiler-Bad Rilchingen": null,
- "Kleinblittersdorf": null,
- "Roßberg": null,
- "Pöllwitz": null,
- "Kargow": null,
- "Kleeth": null,
- "Rostock Seehafen Nord": null,
- "Hoyerswerda-Neustadt": null,
- "Petershain": null,
- "Oberneuschönberg": null,
- "Scheibenberg": null,
- "Thoßfell": null,
- "Vohren": null,
- "Jütrichau": null,
- "Brandstätt": null,
- "Hildbrandsgrün": null,
- "Otzing": null,
- "Runding": null,
- "Schopfheim-Schlattholz": null,
- "Bübingen": null,
- "Brefeld": null,
- "Güdingen": null,
- "Kaiserslautern-Hohenecken": null,
- "Traun": null,
- "Flensburg-Weiche": null,
- "Ahrensfelde (S)": null,
- "Bernau (S)": null,
- "Berlin Charlottenburg (S)": null,
- "Erkner (S)": null,
- "Berlin Friedrichstraße (S)": null,
- "Potsdam Griebnitzsee (S)": null,
- "Hennigsdorf (S)": null,
- "Berlin-Hohenschönhausen (S)": null,
- "Berlin-Karlshorst (S)": null,
- "Berlin-Lichtenberg (S)": null,
- "Oranienburg (S)": null,
- "Berlin Ostbahnhof (S)": null,
- "Potsdam Hbf (S)": null,
- "Berlin-Spandau (S)": null,
- "Colditz": null,
- "Eich(Sachs)": null,
- "Marienberg(Sachs)": null,
- "Penig": null,
- "Plauen(V) unt Bf": null,
- "Rochlitz(Sachs)": null,
- "Straßgräbchen-Bernsdorf": null,
- "Teichwolframsdorf": null,
- "Waldenburg(Sachs)": null,
- "Münster-Mecklenbeck": null,
- "Allendorf(Eder) Bf": null,
- "Bergheim-Giflitz": null,
- "Bürgerhaus, Hessisch Lichtenau": null,
- "Bahnhof Niederzwehren, Kassel": null,
- "ZOB, Duderstadt": null,
- "Dambeck(Altm)": null,
- "Jerichow": null,
- "Nedlitz": null,
- "Sandersdorf(Bitterf)": null,
- "Siedenlangenbeck": null,
- "Sieversdorf(Neust/D)": null,
- "Ziesar": null,
- "Ottobeuren": null,
- "Rohrdorf(Oberbay)": null,
- "München-Süd": null,
- "Merzig(Saar) Ost": null,
- "Vaihingen(Enz)Nord": null,
- "Berka(Wipper)": null,
- "Vacha": null,
- "Friedrichswalde(bei Eberswalde)": null,
- "Ganzlin": null,
- "Klockow(b Waren/Müritz)": null,
- "Milmersdorf": null,
- "Malliß": null,
- "Chop": null,
- "Brest Central": null,
- "Baranovichi Centralnye": null,
- "Minsk-Passajirskii": null,
- "Orscha Central": null,
- "Barchel, Oerel": null,
- "Hamburg Elbbrücken": null,
- "Essel, Kutenholz": null,
- "Fredenbeck": null,
- "Hagen, Stade": null,
- "Hemsen(b Soltau)": null,
- "Kiel-Ellerbek": null,
- "Wagersrott": null,
- "Blankenfelde (S)": null,
- "Berlin-Mahlsdorf": null,
- "Augustusburg Bergstation": null,
- "Buschmühle": null,
- "Einsiedel Brauerei": null,
- "Schmiedeberg-Naundorf": null,
- "Ulberndorf": null,
- "Bornholte(b Verl)": null,
- "Emmerich-Elten": null,
- "Höste": null,
- "Oese": null,
- "Ballersbach": null,
- "Eisemroth": null,
- "Inheiden": null,
- "Altmarkt/Regierungspräsidium, Kassel": null,
- "Am Stern, Kassel": null,
- "Am Kupferhammer, Kassel": null,
- "Forstfeldstraße, Kassel": null,
- "Kirchgasse, Kassel": null,
- "Sandershäuser Straße, Kassel": null,
- "Lindenberg, Kassel": null,
- "Hinter dem Fasanenhof, Kassel": null,
- "Leipziger Platz, Kassel": null,
- "Leipziger Straße, Kassel": null,
- "KVG-Betriebshof, Kassel": null,
- "Friedrichsplatz, Kassel": null,
- "Hegelsbergstraße, Kassel": null,
- "Hauptfriedhof, Kassel": null,
- "Holländischer Platz/Universität, Kassel": null,
- "Holländische Straße, Kassel": null,
- "Königsplatz, Kassel": null,
- "Lutherplatz, Kassel": null,
- "Halitplatz, Kassel": null,
- "Auestadion, Kassel": null,
- "Heinrich-Heine-Straße, Kassel": null,
- "Am Weinberg, Kassel": null,
- "Platz der Deutschen Einheit, Kassel": null,
- "Rathaus, Kassel": null,
- "Rathaus/Fünffensterstraße, Kassel": null,
- "Scheidemannplatz, Kassel": null,
- "Unterneustädter Kirchplatz, Kassel": null,
- "Wilhelmsstraße/Stadtmuseum, Kassel": null,
- "Wiener Straße, Kassel": null,
- "Liebenau(Bz Kassel)": null,
- "Papierfabrik, Kaufungen": null,
- "Arbste": null,
- "Dünsen DHE": null,
- "Groß Ippener DHE": null,
- "Hademstorf": null,
- "Harlesiel": null,
- "Delmenhorst Hasporter Damm": null,
- "Jerxheim": null,
- "Nordhorn-Blanke": null,
- "Neuenhaus Süd": null,
- "Nammen-Bad": null,
- "Stelle DHE": null,
- "Schwindebeck": null,
- "Watenstedt": null,
- "Halle(S) Heidebf": null,
- "Leipzig Essener Straße": null,
- "Meisdorf": null,
- "Feldolling": null,
- "Finsterwald": null,
- "Hammersbach Zugspitzbahn, Grainau": null,
- "Kaltenberg": null,
- "Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc": null,
- "Margertshausen Bf": null,
- "Pfraundorf(Inn)": null,
- "Rosenheim Aicherpark": null,
- "Riffelriß, Grainau": null,
- "Reichertshofen(Schwab) Bf": null,
- "Zainhammer": null,
- "Erbprinz/Schloss, Ettlingen": null,
- "Albgaubad, Ettlingen": null,
- "Neuwiesenreben, Ettlingen": null,
- "Spinnerei, Ettlingen": null,
- "Wasen, Ettlingen": null,
- "Bahnhof, Gönnheim": null,
- "Ittersbach Industrie, Karlsbad": null,
- "Augartenstraße, Karlsruhe": null,
- "Poststraße, Karlsruhe": null,
- "Kronenplatz (Kaiserstraße), Karlsruhe": null,
- "Gottesauer Platz/BGV, Karlsruhe": null,
- "Dammerstock, Karlsruhe": null,
- "Durlach Hubstraße, Karlsruhe": null,
- "Durlach Untermühlstraße, Karlsruhe": null,
- "Weinweg, Karlsruhe": null,
- "Knielingen Eggensteiner Straße, Karlsruhe": null,
- "Händelstraße, Karlsruhe": null,
- "Philippstraße, Karlsruhe": null,
- "Knielingen Herweghstraße, Karlsruhe": null,
- "Knielingen Siemens, Karlsruhe": null,
- "Ettlinger Tor, Karlsruhe": null,
- "Herrenstraße, Karlsruhe": null,
- "Kongresszentrum, Karlsruhe": null,
- "Schillerstraße, Karlsruhe": null,
- "Mühlburg West, Karlsruhe": null,
- "Yorckstraße, Karlsruhe": null,
- "Neureut Bärenweg, Karlsruhe": null,
- "Europapl./Postgalerie (Karl), Karlsruhe": null,
- "Europapl./PostGalerie (Karlstr.), Karlsruhe": null,
- "Europapl./PostGalerie (Kaiserstr), Karlsruhe": null,
- "Lameyplatz, Karlsruhe": null,
- "Starckstraße, Karlsruhe": null,
- "Kullenmühle, Bad Herrenalb": null,
- "Langensteinbach St. Barbara, Karlsbad": null,
- "Ludwigshafen(Rhein) Oppau": null,
- "Reichenbach Kurpark, Waldbronn": null,
- "Rüppurr Battstraße, Karlsruhe": null,
- "Rüppurr Ostendorfplatz, Karlsruhe": null,
- "Rüppurr Tulpenstraße, Karlsruhe": null,
- "Schloss Rüppurr, Karlsruhe": null,
- "Heilbronn Karlstor": null,
- "Großbodungen": null,
- "Gebersdorf": null,
- "Stolberg(Harz)": null,
- "Tiefenort": null,
- "Zopten": null,
- "Alt Schwerin": null,
- "Bantin": null,
- "Bobzin": null,
- "Feldberg(Meckl)": null,
- "Gildenhall": null,
- "Götschendorf": null,
- "Malk Göhren": null,
- "Seebad Lubmin": null,
- "Ringenwalde(Templin)": null,
- "Templin-Ahrensdorf": null,
- "t Harde": null,
- "Aalter": null,
- "Aarau": null,
- "Aarburg-Oftringen": null,
- "Achiet": null,
- "Affoltern am Albis": null,
- "Agde": null,
- "Aigle": null,
- "Aime-la-Plagne": null,
- "Airole": null,
- "Airolo": null,
- "Aix-en-Provence TGV": null,
- "Aix-les-Bains-le-Revard": null,
- "Alassio": null,
- "Alba Iulia": null,
- "Albate-Camerlata": null,
- "Albenga": null,
- "Albertville": null,
- "Alençon": null,
- "Ales": null,
- "Alken(B)": null,
- "Alkmaar": null,
- "Allerheiligenhöfe": null,
- "Almelo": null,
- "Almere Oostvaarders": null,
- "Alphen aan den Rijn": null,
- "Altdorf(CH)": null,
- "Altendorf(CH)": null,
- "Altenmarkt im Pongau": null,
- "Altnau": null,
- "Altstätten SG": null,
- "Amberieu": null,
- "Amersfoort Vathorst": null,
- "Amersfoort": null,
- "Amsterdam Sloterdijk": null,
- "Amsterdam Bijlmer ArenA": null,
- "Amsterdam Centraal": null,
- "Amsterdam Zuid": null,
- "Amsterdam Amstel": null,
- "Amstetten NÖ": null,
- "Andelfingen": null,
- "Andermatt": null,
- "Andorf": null,
- "Angleur": null,
- "Angouleme": null,
- "Ans(B)": null,
- "Antibes": null,
- "Antwerpen Centraal": null,
- "Antwerpen-Zuid": null,
- "Apach(Moselle)": null,
- "Apeldoorn": null,
- "Appingedam": null,
- "Arad": null,
- "Arbon": null,
- "Arbon (See)": null,
- "Arezzo": null,
- "Argeles-sur-Mer": null,
- "Arles": null,
- "Arlon": null,
- "Arnhem Centraal": null,
- "Arnoldstein": null,
- "Arosa": null,
- "Arras(F)": null,
- "Artenay(Loiret)": null,
- "Arth-Goldau": null,
- "Arvant": null,
- "As(CZ)": null,
- "Ashford(Kent)": null,
- "Ashford(Kent) Int.": null,
- "Asse": null,
- "Attnang-Puchheim": null,
- "Au SG": null,
- "Au ZH": null,
- "Aulnoye Aymeries": null,
- "Auneau(Dourdan)": null,
- "Auvelais": null,
- "Avesnes-sur-Helpe": null,
- "Avignon Centre": null,
- "Avignon TGV": null,
- "Babylon": null,
- "Bäch": null,
- "Bad Aussee": null,
- "Bad Blumau": null,
- "Bad Gastein": null,
- "Bad Hofgastein": null,
- "Bad Ischl": null,
- "Bad Ragaz": null,
- "Bad Schallerbach-Wallern": null,
- "Bad Vigaun": null,
- "Bad Zurzach": null,
- "Baden(CH)": null,
- "Baisieux": null,
- "Balerna": null,
- "Banova Jaruga": null,
- "Bantzenheim": null,
- "Banyuls-sur-Mer": null,
- "Barcelona Sants": null,
- "Barendrecht": null,
- "Bar-le-Duc": null,
- "Bascharage-Sanem": null,
- "Basel SBB": null,
- "Basel Dreispitz": null,
- "Basel St Johann": null,
- "Bassersdorf": null,
- "Battipaglia": null,
- "Baunhoej": null,
- "Bayonne": null,
- "Beernem": null,
- "Bekescsaba": null,
- "Bela pod Bezdezem": null,
- "Belfort Ville": null,
- "Bellegarde(Ain)": null,
- "Belleville Meurthe et Moselle": null,
- "Bellinzona": null,
- "Belp": null,
- "Belval Lycée": null,
- "Belval-Rédange": null,
- "Belval-Université": null,
- "Belvaux-Soleuvre": null,
- "Benesov n. Ploucnici": null,
- "Benestroff": null,
- "Benfeld(Selestat)": null,
- "Bening": null,
- "Antwerpen-Berchem": null,
- "Berchem(LUX)": null,
- "Berg(CH)": null,
- "Bergen op Zoom": null,
- "Bergues(Coudek)": null,
- "Bergün/Bravuogn": null,
- "Berlingen(CH)": null,
- "Berlingen URh": null,
- "Bern": null,
- "Bernay": null,
- "Beroun": null,
- "Berthelming": null,
- "Bertrange-Strassen": null,
- "Bertrix": null,
- "Besançon-Mouillère": null,
- "Besançon-Viotte": null,
- "Bettembourg(fr)": null,
- "Bettembourg": null,
- "Bettwiesen": null,
- "Betzdorf(LUX)": null,
- "Bevera": null,
- "Beverwijk": null,
- "Bex": null,
- "Beziers": null,
- "Biarritz": null,
- "Biasca": null,
- "Biberist RBS": null,
- "Biberist Ost": null,
- "Bichlbach Almkopfbahn": null,
- "Bichlbach-Berwang": null,
- "Biel/Bienne": null,
- "Bierset-Awans": null,
- "Bilina": null,
- "Billum st": null,
- "Bilten": null,
- "Bily Kostel nad Nisou": null,
- "Birmensdorf ZH": null,
- "Bischofshofen": null,
- "Blainville-Damelevieres": null,
- "Blaj": null,
- "Le Blanc-Mesnil": null,
- "Blausee-Mitholz": null,
- "Blerick": null,
- "Blindenmarkt": null,
- "Bludenz": null,
- "Böckstein": null,
- "Bodegraven": null,
- "Bodio TI": null,
- "Boen(F)": null,
- "Böheimkirchen": null,
- "Bohumin": null,
- "Bollwiller(Lutterb)": null,
- "Bologna Centrale": null,
- "Bolzano/Bozen": null,
- "Boondael/Boondaal": null,
- "Bordeaux-St-Jean": null,
- "Bordighera": null,
- "Borgo S. Dalmazzo": null,
- "Borne(NL)": null,
- "Borup st": null,
- "Bottighofen": null,
- "Boulevarden st": null,
- "Bourg-en-Bresse": null,
- "Bourges": null,
- "Bourg-St.Maurice": null,
- "Bouzonville": null,
- "Bramming st": null,
- "Brasov": null,
- "Bratislava hl.st.": null,
- "Bratislava-Petrzalka": null,
- "Braunau/Inn": null,
- "Breclav": null,
- "Breclav(Gr)": null,
- "Breda": null,
- "Bredebro st": null,
- "Bregenz": null,
- "Bregenz Hafen": null,
- "Breil-sur-Roya": null,
- "Brennero/Brenner": null,
- "Brescia": null,
- "Bressanone/Brixen": null,
- "Bressoux": null,
- "Brest(F)": null,
- "Breziny u Decina": null,
- "Brig": null,
- "Brixen im Thale": null,
- "Brixlegg": null,
- "Broens st": null,
- "Bronschhofen": null,
- "Bruck/Mur": null,
- "Bruck/Leitha": null,
- "Bruck-Fusch": null,
- "Brugg AG": null,
- "Brugge": null,
- "Brumath": null,
- "Brunico/Bruneck": null,
- "Brunnen(CH)": null,
- "Bruxelles-Midi Eurostar": null,
- "Bruxelles-Central": null,
- "Bruxelles-Luxembourg": null,
- "Bruxelles Midi": null,
- "Brussels Airport - Zaventem": null,
- "Bruxelles-Nord": null,
- "Buchs SG": null,
- "Bucuresti Nord Gara A": null,
- "Budapest-Keleti": null,
- "Budapest-Nyugati": null,
- "Buitenpost": null,
- "Bülach": null,
- "Bully-Grenay": null,
- "Burgfried b.Gnas": null,
- "Bürglen": null,
- "Burgos Rosa de Lima": null,
- "Busigny": null,
- "Bussnang": null,
- "Busto Arsizio": null,
- "Bydgoszcz Glowna": null,
- "Colle Isarco/Gossensass": null,
- "Cadenazzo": null,
- "Calais Ville": null,
- "Calais-Fréthun": null,
- "Caldes de Malavella": null,
- "Campo di Trens/Freienfeld": null,
- "Cannes": null,
- "Cannes-la-Bocca": null,
- "Capellen": null,
- "Capolago-Riva S. Vitale": null,
- "Carbonne(Boussens)": null,
- "Carcassonne": null,
- "Carimate": null,
- "Carnoules(Toulon)": null,
- "Casteldarne/Ehrenburg": null,
- "Castelnaudary": null,
- "Castione-Arbedo": null,
- "Cavaillon(Avignon)": null,
- "Centallo": null,
- "Cents-Hamm": null,
- "Cerbère": null,
- "Cernovice u Chomutova": null,
- "Ceska Kamenice": null,
- "Ceska Kubice": null,
- "Ceska Lipa hl.n.": null,
- "Ceska Lipa strelnice": null,
- "Ceska Trebova": null,
- "Ceske Budejovice": null,
- "Ceske Velenice": null,
- "Chalons en Champagne": null,
- "Chalon sur Saône": null,
- "Chambery-Challes-E": null,
- "Champigneulles": null,
- "Charleroi Sud": null,
- "Château-Arnoux-St-Auban": null,
- "Château du Loir": null,
- "Châteauroux": null,
- "Chateau-Thierry": null,
- "Châtelet": null,
- "Chauny(Tergnier)": null,
- "Cheb": null,
- "Cheb-Skalka": null,
- "Chelles Gournay": null,
- "Chenay Gagny": null,
- "Chenee": null,
- "Cherbourg": null,
- "Chiasso": null,
- "Chiusa/Klausen": null,
- "Chiusi-Chianciano Terme": null,
- "Chomutov": null,
- "Chomutov mesto": null,
- "Chotyne": null,
- "Chrastava": null,
- "Chrastava-Andelska Hora": null,
- "Chribska": null,
- "Chur": null,
- "Cintegabelle": null,
- "Clermont-Ferrand": null,
- "Clerval": null,
- "Clervaux": null,
- "Coevorden": null,
- "Collioure": null,
- "Colmar": null,
- "Combs la Ville Quincy": null,
- "Como S. Giovanni": null,
- "Compiegne": null,
- "Conegliano": null,
- "Conflans-Jarny": null,
- "Corbehem(Douai)": null,
- "Corbeil Essonnes": null,
- "Cornaux": null,
- "Cosne": null,
- "Cossonay-Penthalaz": null,
- "Coulommiers": null,
- "Courcelles-sur-Nied": null,
- "Coutras": null,
- "Creil": null,
- "Culmont-Chalindrey": null,
- "Culoz": null,
- "Cuneo": null,
- "Curtici": null,
- "Czechowice-Dziedzice": null,
- "Czerwiensk": null,
- "Dachsen": null,
- "Dagmersellen": null,
- "Däniken": null,
- "Davos Dorf": null,
- "Davos Platz": null,
- "Dax": null,
- "Enschede De Eschmarke": null,
- "Debrecen": null,
- "Decin hl.n.": null,
- "Decin vychod": null,
- "Decin-Certova voda": null,
- "Decin-Priper": null,
- "Decin-Prostredni Zleb": null,
- "Delden": null,
- "Delémont": null,
- "Delft": null,
- "Delfzijl": null,
- "Den Haag Centraal": null,
- "Den Haag HS": null,
- "Den Helder": null,
- "Denderleeuw": null,
- "Desenice": null,
- "Desenzano del Garda/Sirmione": null,
- "Desio": null,
- "Dettwiller": null,
- "Deva": null,
- "Deventer": null,
- "Devinska Nova Ves": null,
- "Diano Marina": null,
- "Diemeringen": null,
- "Diepenbeek": null,
- "Diessenhofen": null,
- "Diessenhofen URh": null,
- "Dietlikon": null,
- "Dieulouard": null,
- "Differdange": null,
- "Dijon Ville": null,
- "Dijon Porte Neuve": null,
- "Dippach-Reckange": null,
- "Dobbiaco/Toblach": null,
- "Dobova": null,
- "Dobova(Gr)": null,
- "Doestrup(Soenderjylland) st": null,
- "Doksy": null,
- "Dole Ville": null,
- "Dolhain-Gileppe": null,
- "Dolni Habartice": null,
- "Dolni Podluzi": null,
- "Dolni Poustevna": null,
- "Dolni Zleb": null,
- "Dolni Zleb zast.": null,
- "Domazlice": null,
- "Domina": null,
- "Dommeldange": null,
- "Domodossola": null,
- "Dordrecht": null,
- "Dorfgastein": null,
- "Dornbirn": null,
- "Dornbirn Schoren": null,
- "Dottikon-Dintikon": null,
- "Dourges": null,
- "Drauffelt": null,
- "Dronten": null,
- "Duchcov": null,
- "Dugo Selo": null,
- "Duivendrecht": null,
- "Dunkerque": null,
- "Dyreby st": null,
- "Ebbsfleet International Eurostar": null,
- "Eben im Pongau": null,
- "Ebenfurth": null,
- "Ebersheim": null,
- "Ede(B)": null,
- "Ede Centrum": null,
- "Ede-Wageningen": null,
- "Effretikon": null,
- "Eglisau": null,
- "Egnach": null,
- "Ehrwald Zugspitzbahn": null,
- "Eindhoven": null,
- "Einsiedeln": null,
- "Elne": null,
- "Elsbethen": null,
- "Emmenbrücke": null,
- "Engis": null,
- "Enns": null,
- "Enschede": null,
- "Epernay": null,
- "Epierre-St Leger": null,
- "Epinal": null,
- "Erlen": null,
- "Ermatingen": null,
- "Ermatingen URh": null,
- "Erquelinnes": null,
- "Erstein": null,
- "Erstfeld": null,
- "Esbjerg st": null,
- "Eschenau/Salzach": null,
- "Esch-sur-Alzette": null,
- "Essen(B)": null,
- "Etampes": null,
- "Ettelbruck": null,
- "Etzwilen": null,
- "Eupen": null,
- "Evreux Normandie": null,
- "Eygelshoven Markt": null,
- "Faak am See": null,
- "Faido": null,
- "Faulquemont": null,
- "Fegersheim Lipsheim": null,
- "Fehraltorf": null,
- "Feldkirch": null,
- "Feldkirchen in Kärnten": null,
- "Felixdorf": null,
- "Budapest-Ferencváros": null,
- "Ferrara": null,
- "Fieberbrunn": null,
- "Figueres": null,
- "Filisur": null,
- "Finale Ligure Marina": null,
- "Firenze S.M.N.": null,
- "Flamatt": null,
- "Flassa": null,
- "Flaurling": null,
- "Flers": null,
- "Wien Floridsdorf": null,
- "Flüelen": null,
- "Flums": null,
- "Föderlach": null,
- "Fontaine": null,
- "Fontainebleau-Avon": null,
- "Fontan Saorge": null,
- "Forbach(F)": null,
- "Forest Midi/Vorst Zuid": null,
- "Fortezza/Franzensfeste": null,
- "Fossano": null,
- "Fourchambault": null,
- "Fraipont": null,
- "Frankenmarkt": null,
- "Frantiskovy Lazne Aquaforum": null,
- "Frantiskovy Lazne": null,
- "Frastanz": null,
- "Frauenfeld": null,
- "Fredericia st": null,
- "Freienbach SBB": null,
- "Frenkendorf-Füllinsdorf": null,
- "Fribourg/Freiburg": null,
- "Friesach in Kärnten": null,
- "Frisvadvej st": null,
- "Fritzens-Wattens": null,
- "Frontenex": null,
- "Frouard": null,
- "Frutigen": null,
- "Fürnitz": null,
- "Futuroscope": null,
- "Gagny": null,
- "Gaillon Aubevoye": null,
- "Gallarate": null,
- "Gampel-Steg": null,
- "Gandrange-Amneville": null,
- "Gannat": null,
- "Gänserndorf": null,
- "Gardanne": null,
- "Gdansk Glowny": null,
- "Gdansk Oliwa": null,
- "Gdansk Wrzeszcz": null,
- "Gdynia Glowna": null,
- "Geinberg": null,
- "Geleen-Lutterade": null,
- "Gelterkinden": null,
- "Gemona del Friuli": null,
- "Genève": null,
- "Genève-Aéroport": null,
- "Genk": null,
- "Genova Piazza Principe": null,
- "Gentbrugge": null,
- "Gent-Dampoort": null,
- "Gent St Pieters": null,
- "Gerlafingen": null,
- "Gerling im Pinzgau": null,
- "Girona": null,
- "Gevrey-Chambertin": null,
- "Gießenbach in Tirol": null,
- "Gisikon-Root": null,
- "Gisors Embranchement": null,
- "Giubiasco": null,
- "Gjesing st": null,
- "Glanerbrug": null,
- "Glattbrugg": null,
- "Glattfelden": null,
- "Gloggnitz": null,
- "Gmünd NÖ": null,
- "Gniezno": null,
- "Goebelsmühle": null,
- "Golling-Abtenau": null,
- "Goppenstein": null,
- "Gorzow Wlkp.": null,
- "Göschenen": null,
- "Gossau SG": null,
- "Gottlieben (Schifflände)": null,
- "Götzendorf/Leitha": null,
- "Götzis": null,
- "Gouda": null,
- "Gouvy": null,
- "Graffenstaden": null,
- "Gramatneusiedl": null,
- "Granollers": null,
- "Gratwein-Gratkorn": null,
- "Graz Hbf": null,
- "Graz Ostbahnhof-Messe": null,
- "Gredstedbro st": null,
- "Grenchen Nord": null,
- "Grenoble": null,
- "Gresy-sur-Isere": null,
- "Gretz-Armainvilliers": null,
- "Gries am Brenner": null,
- "Gries im Pinzgau": null,
- "Grieskirchen-Gallspach": null,
- "Gröbming": null,
- "Groenendaal": null,
- "Groningen": null,
- "Schwechat": null,
- "Guldager st": null,
- "Guntramsdorf Kaiserau": null,
- "Gurten OÖ": null,
- "Güttingen": null,
- "Györ": null,
- "Haaltert": null,
- "Haarlem": null,
- "Haarlem Spaarnwoude": null,
- "Häggenschwil-Winden": null,
- "Hagenau im Innkreis": null,
- "Hagondange": null,
- "Haguenau": null,
- "Haiming": null,
- "Hall in Tirol": null,
- "Hallein": null,
- "Hallwang-Elixhausen": null,
- "Hard-Fussach": null,
- "Hasselt": null,
- "Hatting in Tirol": null,
- "Hauptwil": null,
- "Haus im Ennstal": null,
- "Hayange": null,
- "Hazebrouck": null,
- "Heerbrugg": null,
- "Heerenveen": null,
- "Heerlen": null,
- "Hegyeshalom(Gr)": null,
- "Hegyeshalom": null,
- "Heiterwang-Plansee": null,
- "Helmond": null,
- "Hendaye": null,
- "Hendschiken": null,
- "Hengelo": null,
- "Henin-Beaumont": null,
- "Henne st": null,
- "Herentals": null,
- "Hergenrath": null,
- "Wien Hernals": null,
- "Herny": null,
- "Herrlisheim près Colmar": null,
- "Herstal": null,
- "Herzele": null,
- "Herzogenbuchsee": null,
- "Herzogenburg": null,
- "Hillegom": null,
- "Hilversum": null,
- "Hilversum Sportpark": null,
- "Hnevice": null,
- "Hochfelden": null,
- "Hochfilzen": null,
- "Hochzirl": null,
- "Hoeilaart": null,
- "Hoeje Taastrup st": null,
- "Hohenau": null,
- "Hohenems": null,
- "Hombourg-Haut": null,
- "Hoofddorp": null,
- "Hoogeveen": null,
- "Hoogezand-Sappemeer": null,
- "Hopfgarten im Brixental": null,
- "Hopfgarten im Brixental Berglift": null,
- "Horgen": null,
- "Horn(Bodensee)": null,
- "Horn(Bodensee), SF": null,
- "Horni Blatna": null,
- "Horni Dvoriste": null,
- "Horni Kamenice": null,
- "Horni Podluzi": null,
- "Horni Poustevna": null,
- "Horovice": null,
- "Hörsching": null,
- "Hradek nad Nisou": null,
- "Hranice na Morave": null,
- "Hrebeny": null,
- "Hüntwangen-Wil": null,
- "Hüttau": null,
- "Huy(B)": null,
- "Hviding st": null,
- "Hyllerslev st": null,
- "Ilawa Glowna": null,
- "Immensee": null,
- "Imsterberg": null,
- "Imst-Pitztal": null,
- "Ingelmunster": null,
- "Ingwiller": null,
- "Innsbruck Hbf": null,
- "Innsbruck Hötting": null,
- "Innsbruck Westbahnhof": null,
- "Inowroclaw": null,
- "Interlaken Ost": null,
- "Interlaken West": null,
- "Inzing/Inn": null,
- "Irun": null,
- "Is-sur-Tille": null,
- "Iselle di Trasquera": null,
- "Iselle transito": null,
- "Ivanic Grad": null,
- "Jablonne v Podjestedi": null,
- "Janderup st": null,
- "Jankowa Zaganska": null,
- "Janovice nad Uhlavou": null,
- "Wien Jedlersdorf": null,
- "Jedlova": null,
- "Jegum st": null,
- "Jelenia Gora": null,
- "Rochefort-Jemelle": null,
- "Jenbach": null,
- "Jenbach Zillertalbahn": null,
- "Jesenice(SL)": null,
- "Jestetten": null,
- "Jeumont": null,
- "Jiretin pod Jedlovou": null,
- "Jirkov zast.": null,
- "Joeuf": null,
- "Joigny(Lar.Migennes)": null,
- "Judenburg": null,
- "Juvisy": null,
- "Kadan-Prunerov": null,
- "Kaiseraugst": null,
- "Kalhausen": null,
- "Kalsdorf b.Graz": null,
- "Kalwang": null,
- "Kampen Zuid": null,
- "Kandersteg": null,
- "Kapfenberg": null,
- "Karlovy Vary dolni n.": null,
- "Karlovy Vary": null,
- "Katowice": null,
- "Kautenbach": null,
- "Kehlhof": null,
- "Kelebia": null,
- "Kelenföld": null,
- "Kematen in Tirol": null,
- "Kesswil": null,
- "Kesteren": null,
- "Kilchberg(CH)": null,
- "Killwangen-Spreitenbach": null,
- "Kindberg": null,
- "Kirchberg in Tirol": null,
- "Kirchbichl": null,
- "Kittsee": null,
- "Kitzbühel Hahnenkamm": null,
- "Kitzbühel": null,
- "Klagenfurt Hbf": null,
- "Klatovy": null,
- "Kledering b.Wien": null,
- "Wien Kaiserebersdorf": null,
- "Kleinbettingen": null,
- "Kloten": null,
- "Knittelfeld": null,
- "Koblenz(CH)": null,
- "Koebenhavn H": null,
- "Koebenhavns Lufthavn st": null,
- "Koege st": null,
- "Kogenheim": null,
- "Kolbnitz": null,
- "Kolding st": null,
- "Kolin(CZ)": null,
- "Komarom": null,
- "Konin": null,
- "Koog aan de Zaan": null,
- "Zaandijk Zaanse Schans": null,
- "Körmend": null,
- "Korsoer st": null,
- "Kortenberg": null,
- "Kortrijk": null,
- "Kovarska": null,
- "Krakow Glowny": null,
- "Kralupy nad Vltavou": null,
- "Kranebitten": null,
- "Kranj": null,
- "Kraslice": null,
- "Kraslice-Pod vlekem": null,
- "Kraslice predmesti": null,
- "Krasna Lipa": null,
- "Krasna Lipa mesto": null,
- "Krems an der Donau": null,
- "Kreuzlingen Bernrain": null,
- "Kreuzlingen": null,
- "Kreuzlingen Hafen": null,
- "Kirchdorf/Krems": null,
- "Krimmeri-Meinau": null,
- "Krimov": null,
- "Krommenie-Assendelft": null,
- "Kropswolde": null,
- "Krsko": null,
- "Krumpendorf/Wörthersee": null,
- "Krzewina Zgorzelecka": null,
- "Kuchl": null,
- "Kufstein": null,
- "Kundl": null,
- "Kunowice": null,
- "Küssnacht am Rigi": null,
- "Kutina": null,
- "Kutno": null,
- "Kuty": null,
- "Kytlice": null,
- "La Bastide-St-Laurent les Bains": null,
- "La Brigue(F)": null,
- "La Charité sur Loire": null,
- "La Plaine": null,
- "La Roche sur Yon": null,
- "La Souterraine": null,
- "Laa/Thaya": null,
- "Lachen": null,
- "Lage Zwaluwe": null,
- "Lähn": null,
- "LAigle": null,
- "Lamadelaine": null,
- "Lambach": null,
- "Lamone-Cadempino": null,
- "Landeck-Zams": null,
- "Landen": null,
- "Landgraaf": null,
- "Landquart": null,
- "Landry": null,
- "Langdorp": null,
- "Langen am Arlberg": null,
- "Langenthal(CH)": null,
- "Langkampfen": null,
- "Läufelfingen": null,
- "Laufen(CH)": null,
- "Laufenburg(CH)": null,
- "Lausanne": null,
- "Lausanne-Flon": null,
- "Lausen(CH)": null,
- "Lauterach": null,
- "Lauterbourg": null,
- "Laveno Mombello": null,
- "Le Bourget": null,
- "Le Creusot Montceau Montchanin TGV": null,
- "Le Havre": null,
- "Le Mans": null,
- "Le Raincy Villemomble Montferm": null,
- "Lebbeke": null,
- "Leeuwarden": null,
- "Legnica": null,
- "Leibnitz": null,
- "Leiden Centraal": null,
- "Leiden Lammenschans": null,
- "Leithen b.Seefeld": null,
- "Lelystad Centrum": null,
- "Lend": null,
- "Lengwil": null,
- "Lens(F)": null,
- "Lenzburg": null,
- "Lenzing": null,
- "Leoben Hbf": null,
- "Leogang": null,
- "Leopoldsburg": null,
- "Lermoos": null,
- "Lerouville": null,
- "Les Arcs Draguignan": null,
- "Les-Aubrais-Orleans": null,
- "Lesce-Bled": null,
- "Leudelange": null,
- "Leuk": null,
- "Leuven": null,
- "Lezignan": null,
- "Liberec": null,
- "Liège-Guillemins": null,
- "Lienz in Osttirol": null,
- "Liers": null,
- "Liestal": null,
- "Liezen": null,
- "Lille Flandres": null,
- "Lille Europe": null,
- "Limone": null,
- "Linz Hbf": null,
- "Linz/Donau Wegscheid": null,
- "Lipinki Luzyckie": null,
- "Lipova u Sluknova": null,
- "Litija": null,
- "Litomerice mesto": null,
- "Livorno Centrale": null,
- "Ljubljana": null,
- "Locarno": null,
- "Lochau-Hörbranz": null,
- "Loeftgaard st": null,
- "Lököshaza": null,
- "Longueau": null,
- "Longwy": null,
- "Lons-Le-Saunier": null,
- "Loosdorf b.Melk": null,
- "Lorraine": null,
- "Lottstetten": null,
- "Lourches": null,
- "Lourdes": null,
- "Lovosice": null,
- "Luban Sl.": null,
- "Ludesch": null,
- "Lugano": null,
- "Luh nad Svatavou": null,
- "Luino": null,
- "Lumes Halte": null,
- "Lunde J st": null,
- "Lunderskov st": null,
- "Lunel": null,
- "Lunéville": null,
- "Lupfig": null,
- "Lustenau": null,
- "Luterbach-Attisholz": null,
- "Lutterbach": null,
- "Lutzelbourg": null,
- "Luxembourg": null,
- "Luzern": null,
- "Lyon Part Dieu": null,
- "Maastricht": null,
- "Machnin": null,
- "Machnin hrad": null,
- "Mâcon Ville": null,
- "Mâcon-Loché TGV": null,
- "Maienfeld": null,
- "Maishofen-Saalbach": null,
- "Mala Velen": null,
- "Malbork": null,
- "Malczyce": null,
- "Mallnitz-Obervellach": null,
- "Malmö Central": null,
- "Mamer": null,
- "Mamer Lycée": null,
- "Mammern(Bodensee)": null,
- "Mammern URh": null,
- "Manage": null,
- "Mannenbach-Salenstein": null,
- "Mannenbach URh": null,
- "Manternach": null,
- "Marchegg": null,
- "Marche-les-Dames": null,
- "Marchienne au Pont": null,
- "Marchtrenk": null,
- "Maribor": null,
- "Markvartice": null,
- "Marle-sur-Serre": null,
- "Marloie": null,
- "Marne la Vallée-Chessy": null,
- "Marseille-Blancarde": null,
- "Marseille-St-Charles": null,
- "Martenshoek": null,
- "Martigny": null,
- "Martigues": null,
- "Märwil": null,
- "Matrei am Brenner": null,
- "Maubeuge": null,
- "Mautern im Liesingtal": null,
- "Mayrhofen im Zillertal": null,
- "Mechelen": null,
- "Medias": null,
- "Meiringen": null,
- "Melk": null,
- "Mels": null,
- "Melun": null,
- "Mendrisio": null,
- "Menton": null,
- "Menznau": null,
- "Merano/Meran": null,
- "Merchtem": null,
- "Merelbeke": null,
- "Mersch(LUX)": null,
- "Mertert": null,
- "Merxheim(Colmar)": null,
- "Raaba": null,
- "Metz Ville": null,
- "Meuse TGV": null,
- "Michelau(LUX)": null,
- "Micheldorf": null,
- "Middelfart st": null,
- "Miekinia": null,
- "Mikulasovice dol.n.": null,
- "Milano Centrale": null,
- "Milano Greco Pirelli": null,
- "Milano Porta Garibaldi": null,
- "Milmort": null,
- "Mimon": null,
- "Mining": null,
- "Miramas": null,
- "Mitterberghütten": null,
- "Mitterdorf-Veitsch": null,
- "Mixnitz Bärenschützklamm": null,
- "Mlada Boleslav hl.n.": null,
- "Mlyny(CZ)": null,
- "Modane": null,
- "Möhlin": null,
- "Mol": null,
- "Mols": null,
- "Mommenheim": null,
- "Monaco-Monte-Carlo": null,
- "Monguelfo-Casies/Welsberg-Gsies": null,
- "Mons": null,
- "Montbéliard Ville": null,
- "Montelimar": null,
- "Monthey": null,
- "Montluçon Ville": null,
- "Montmelian": null,
- "Montpellier Saint-Roch": null,
- "Montreux": null,
- "Monza": null,
- "Moosbierbaum-Heiligeneich": null,
- "Moret-Veneux-les-Sablons": null,
- "Morges": null,
- "Morhange": null,
- "Moskva Belorusskaja": null,
- "Mosonmagyarovar": null,
- "Most": null,
- "Mötz": null,
- "Mouchard": null,
- "Moulins-sur-Allier": null,
- "Mouscron": null,
- "Moustier": null,
- "Moutier": null,
- "Moutiers-Salins-Brides-les-Bains": null,
- "Moyeuvre-Grande": null,
- "Mühlehorn": null,
- "Mühlheim am Inn": null,
- "Mühldorf-Möllbrücke": null,
- "Muizen": null,
- "Mulhouse-Dornach": null,
- "Mulhouse Ville": null,
- "Münchenbuchsee": null,
- "Mundolsheim": null,
- "Munkzwalm": null,
- "Munsbach": null,
- "Münsingen(CH)": null,
- "Munster(Metzeral)": null,
- "Münster-Wiesing": null,
- "Münsterlingen-Scherzingen": null,
- "Muolen": null,
- "Murg(CH)": null,
- "Mürzzuschlag": null,
- "Musau": null,
- "Mussidan": null,
- "Muttenz": null,
- "Naestved st": null,
- "Nagymaros-Visegrad": null,
- "Namur": null,
- "Nancois Tronville": null,
- "Nancy Ville": null,
- "Nantes": null,
- "Narbonne": null,
- "Nebikon": null,
- "Neerpelt": null,
- "Nejdek": null,
- "Nejdek zastavka": null,
- "Nemours St Pierre": null,
- "Nendeln": null,
- "Nenzing": null,
- "Nessonvaux": null,
- "Nestedice": null,
- "Nestemice": null,
- "Nettingsdorf": null,
- "Neuchâtel": null,
- "Neufchateau(B)": null,
- "Neufchateau(F)": null,
- "Neuhausen(CH)": null,
- "Neukirch-Egnach": null,
- "Neumarkt-Kallham": null,
- "Neumarkt-Köstendorf": null,
- "Neuratting": null,
- "Nice Ville": null,
- "Niederbipp": null,
- "Niederglatt": null,
- "Niederkorn": null,
- "Nieuw Amsterdam": null,
- "Bad Nieuweschans": null,
- "Nijmegen": null,
- "Nîmes": null,
- "Noerre Nebel st": null,
- "Noerreport st": null,
- "Noertzange": null,
- "Nogent-le-Rotrou": null,
- "Noisy-le-Sec": null,
- "Notre-Dame-de-Briancon": null,
- "Nova Gradiska": null,
- "Nova Kapela": null,
- "Nova Role": null,
- "Nova Role zastavka": null,
- "Novara": null,
- "Nove Hamry": null,
- "Nejdek-Sejfy": null,
- "Nove Zamky": null,
- "Noveant": null,
- "Novi Beograd": null,
- "Novi Sad": null,
- "Novska": null,
- "Novy Bor": null,
- "Noyon": null,
- "Ny Ellebjerg st": null,
- "Nyborg st": null,
- "Nyiregyhaza": null,
- "Nykoebing F st": null,
- "Nymburk hl.n.": null,
- "Oberalm": null,
- "Oberburg": null,
- "Oberglatt": null,
- "Oberhofen im Inntal": null,
- "Oberkorn": null,
- "Obermodern": null,
- "Obernberg-Altheim": null,
- "Oberrieden(CH)": null,
- "Oberriet": null,
- "Obourg": null,
- "Odense st": null,
- "Oermingen": null,
- "Oesterport st": null,
- "Oetrange": null,
- "Oksboel st": null,
- "Okucani": null,
- "Oldenzaal": null,
- "Nejdek-Oldrichov": null,
- "Olen": null,
- "Olivetta-S. Michele": null,
- "Olovi": null,
- "Olten": null,
- "Ommen": null,
- "Onville": null,
- "Oostende": null,
- "Opole Glowne": null,
- "Oppikon": null,
- "Opwijk": null,
- "Orange(Avignon)": null,
- "Orchies": null,
- "Orléans": null,
- "Orvieto": null,
- "Oss": null,
- "Ostermundigen": null,
- "Ostrava hl.n.": null,
- "Ostrava-Svinov": null,
- "Oswiecim": null,
- "Othmarsingen": null,
- "Ötztal": null,
- "Outrup st": null,
- "Overveen": null,
- "Ponte Gardena-Laion/Waidbruck-Lajen": null,
- "Padborg st": null,
- "Padova": null,
- "Pagny-sur-Moselle": null,
- "Pantin": null,
- "Pardubice hl.n.": null,
- "Paris Montparnasse": null,
- "Paris Nord": null,
- "Paris Austerlitz": null,
- "Paris Est": null,
- "Paris Gare de Lyon": null,
- "Paris St Lazare": null,
- "Parndorf": null,
- "Paternion-Feistritz": null,
- "Patsch": null,
- "Pau": null,
- "Pavia": null,
- "Peggau-Deutschfeistritz": null,
- "Peltre": null,
- "Pepinster": null,
- "Perigueux": null,
- "Pernink": null,
- "Perpignan": null,
- "Peschiera del Garda": null,
- "Petange": null,
- "Peterskirchen": null,
- "Petit Croix": null,
- "Pfäffikon SZ": null,
- "Pfarrwerfen": null,
- "Pflach": null,
- "Pichl b.Schladming": null,
- "Piensk": null,
- "Pill-Vomperbach": null,
- "Pino transito": null,
- "Pisa Centrale": null,
- "Plesna(CZ)": null,
- "Ploiesti Vest": null,
- "Plzen hl.n.": null,
- "Pöchlarn": null,
- "Poitiers": null,
- "Pomezi nad Ohri": null,
- "Pont-a-Mousson": null,
- "Pontresina": null,
- "Pont-Ste-Maxence": null,
- "Pont St Vincent": null,
- "Pordenone": null,
- "Port Vendres Ville": null,
- "Port Bou": null,
- "Pörtschach am Wörther See": null,
- "Potucky": null,
- "Potucky zastavka": null,
- "Pougues les Eaux": null,
- "Poznan Gl.": null,
- "Praha hl.n.": null,
- "Praha-Holesovice": null,
- "Praha-Smichov": null,
- "Pram-Haag": null,
- "Pratteln": null,
- "Predeal": null,
- "Pregarten": null,
- "Prerov": null,
- "Prinzersdorf": null,
- "Profondsart": null,
- "Przylep": null,
- "Puch bei Hallein": null,
- "Pusarnitz": null,
- "Quevy": null,
- "Quimper": null,
- "Radstadt": null,
- "Rafz": null,
- "Rankweil": null,
- "Ranzo-S. Abbondio": null,
- "Rattenberg-Kramsach": null,
- "Re(I)": null,
- "Rebstein-Marbach": null,
- "Réding(F)": null,
- "Reichenbach im Kandertal": null,
- "Reichenburg": null,
- "Reims": null,
- "Reith b.Seefeld": null,
- "Rejsby st": null,
- "Rekawinkel": null,
- "Rekingen AG": null,
- "Rémelfing": null,
- "Rémilly": null,
- "Remiremont": null,
- "Rennes": null,
- "Retenice": null,
- "Retz": null,
- "Reutte in Tirol": null,
- "Reutte in Tirol Schulzentrum": null,
- "Rheineck": null,
- "Rheinfelden(CH)": null,
- "Ribe st": null,
- "Ribe Noerremark st": null,
- "Richterswil": null,
- "Ried im Innkreis": null,
- "Bregenz Riedenburg": null,
- "Rietz in Tirol": null,
- "Rimini": null,
- "Ringsted st": null,
- "Rio di Pusteria/Mühlbach": null,
- "Rivera-Bironico": null,
- "Rivesaltes": null,
- "Roanne": null,
- "Robilante": null,
- "Roccavione": null,
- "Rodange": null,
- "Roedby": null,
- "Roedekro st": null,
- "Roeschwoog": null,
- "Roggwil-Berg": null,
- "Roggwil-Wynau": null,
- "Rohr-Bad Hall": null,
- "Rokycany": null,
- "Roma Termini": null,
- "Romanshorn": null,
- "Romanshorn (See)": null,
- "Rombas-Clouange": null,
- "Ronet": null,
- "Roodt/Syre": null,
- "Roosendaal": null,
- "Roppen": null,
- "Rorschach": null,
- "Rorschach Hafen": null,
- "Rorschach Hafen (See)": null,
- "Rosenbach bei Villach": null,
- "Jesenice(Gr)": null,
- "Roskilde st": null,
- "Rotava": null,
- "Rothenburg(CH)": null,
- "Rothenthurm(CH)": null,
- "Rothrist": null,
- "Rotkreuz": null,
- "Rotterdam Lombardijen": null,
- "Rotterdam Noord": null,
- "Rotterdam Centraal": null,
- "Rotterdam Stadion": null,
- "Roudnice nad Labem": null,
- "Rouffach": null,
- "Rovereto": null,
- "Rovigo": null,
- "Rum b.Innsbruck": null,
- "Ruma": null,
- "Rumburk": null,
- "Rümlang": null,
- "Rupperswil": null,
- "Rüschlikon": null,
- "Rusova": null,
- "Rüthi SG": null,
- "Rybniste": null,
- "Rzepin": null,
- "St-Avre-la-Chambre": null,
- "St-Michel-Valloire": null,
- "St.Peter-Seitenstetten": null,
- "San Candido/Innichen": null,
- "Saalfelden": null,
- "Safenwil": null,
- "Saincaize": null,
- "Saintes": null,
- "Saint Ghislain": null,
- "Salez-Sennwald": null,
- "Salzburg Aiglhof": null,
- "Salzburg Liefering": null,
- "Salzburg Aigen": null,
- "Salzburg Hbf": null,
- "Salzburg Parsch": null,
- "Salzburg Sam": null,
- "Salzburg Süd": null,
- "Salzburg Gnigl": null,
- "Samedan": null,
- "Samstagern": null,
- "San Remo": null,
- "Sandweiler-Contern": null,
- "Sanry-sur-Nied": null,
- "Sapjane(Gr)": null,
- "Sappemeer Oost": null,
- "Sargans": null,
- "Sarrebourg": null,
- "Sarreguemines": null,
- "Sarreinsming": null,
- "Sathonay Rillieux": null,
- "Saumur Rive Droit": null,
- "Saverne": null,
- "Savona": null,
- "Salzburg Mülln-Altstadt": null,
- "Schaan-Vaduz": null,
- "Schaerbeek": null,
- "Schaffhausen": null,
- "Schaftenau": null,
- "Schärding": null,
- "Scharnitz": null,
- "Scheemda": null,
- "Schiedam Centrum": null,
- "Schifflange": null,
- "Schindellegi-Feusisberg": null,
- "Schiphol (Airport)": null,
- "Schladming": null,
- "Schönbichl in Tirol": null,
- "Schönwies": null,
- "Schouweiler": null,
- "Schruns": null,
- "Schübelbach-Buttikon": null,
- "Schulen": null,
- "Schwarzach-St.Veit": null,
- "Schwaz": null,
- "Schwindratzheim": null,
- "Schwyz": null,
- "Scuol-Tarasp": null,
- "Sebuzin": null,
- "Seefeld in Tirol": null,
- "Seekirchen am Wallersee": null,
- "Sejstrup st": null,
- "Sélestat": null,
- "Selzthal": null,
- "Seregno": null,
- "Sesto S. Giovanni": null,
- "Sete": null,
- "Sevelen": null,
- "Sevnica": null,
- "s-Hertogenbosch": null,
- "Sid(SRB)": null,
- "Siebnen-Wangen": null,
- "Siegershausen": null,
- "Sieniawa Zarska": null,
- "Sierck-les-Bains": null,
- "Sierre/Siders": null,
- "Sighisoara": null,
- "Sillian": null,
- "Silz im Oberinntal": null,
- "Simeria": null,
- "Sinaia": null,
- "Sion": null,
- "Sissach": null,
- "Sittard": null,
- "Skaerbaek st": null,
- "Skofja Loka": null,
- "Slagelse st": null,
- "Slavonski Brod": null,
- "Slubice": null,
- "Sluknov": null,
- "Sluknov zast.": null,
- "Smolensk": null,
- "Sneek": null,
- "Sokolov": null,
- "Solothurn": null,
- "Somain": null,
- "Sopot": null,
- "Sopron": null,
- "Soroe st": null,
- "Sosnowiec Glowny": null,
- "Spa": null,
- "Spangsbjerg st": null,
- "Spicak": null,
- "Spielfeld-Straß": null,
- "Spiez": null,
- "Spital am Pyhrn": null,
- "Spittal-Millstättersee": null,
- "Sremska Mitrovica": null,
- "St Dalmas de Tende": null,
- "St. Gallen(CH)": null,
- "St. Gallen(CH) Winkeln": null,
- "St. Gallen(CH) Haggen": null,
- "St-Jory(Toulouse)": null,
- "St Malo": null,
- "St.Jodok am Brenner": null,
- "St. Moritz": null,
- "St. Anton am Arlberg": null,
- "St. Anton im Montafon": null,
- "St. Johann im Pongau": null,
- "St. Johann in Tirol": null,
- "St. Margrethen": null,
- "St.Michael in Obersteiermark": null,
- "St.Pölten Hbf": null,
- "St. Valentin": null,
- "St.Veit/Glan": null,
- "Staad": null,
- "Wien Stadlau": null,
- "Stadt Rottenmann": null,
- "Staffelfelden": null,
- "Stainach-Irdning": null,
- "St-Amour": null,
- "Stams": null,
- "Stans bei Schwaz": null,
- "Stara Pazova": null,
- "Stara Role": null,
- "Statte": null,
- "St Avold": null,
- "Sint-Denijs-Boekel": null,
- "Steckborn": null,
- "Steckborn URh": null,
- "Steenwijk": null,
- "Steinach in Tirol": null,
- "Steinbourg": null,
- "Steindorf bei Straßwalchen": null,
- "Steinebrunn": null,
- "Stein-Säckingen": null,
- "Stephansfeld": null,
- "Stettbach": null,
- "Steyr": null,
- "St-Germain-des-Fosses": null,
- "St-Gervais-les-Bains": null,
- "St-Hilaire-au-Temple": null,
- "St-Jean-de-Luz-Ciboure": null,
- "St-Jean-de-Maurienne Arvan": null,
- "St-Louis (Haut-Rhin)": null,
- "St-Maurice(CH)": null,
- "Stockerau": null,
- "St-Pierre-dAlbigny": null,
- "St-Pierre-des-Corps": null,
- "St-Priest": null,
- "St-Quentin(Aisne)": null,
- "St-Raphael-Valescure": null,
- "Strasbourg": null,
- "Strasshof": null,
- "Straßwalchen": null,
- "Stresa": null,
- "Strizivojna-Vrpolje": null,
- "St-Sulpice-Lauriere": null,
- "Sturovo": null,
- "Subotica": null,
- "Nejdek-Sucha": null,
- "Summerau": null,
- "Sursee": null,
- "Survilliers Fosses": null,
- "Wien Süßenbrunn": null,
- "Susteren": null,
- "Svatava": null,
- "Svatava zastavka": null,
- "Svor": null,
- "Swiebodzin": null,
- "Swinoujscie Centrum": null,
- "Szczecin Glowny": null,
- "Szczecin Gumience": null,
- "Szentgotthárd": null,
- "Szob": null,
- "Szob(Gr)": null,
- "Szolnok": null,
- "Tabor": null,
- "Taggia": null,
- "Tamines": null,
- "Tarascon sur Rhone": null,
- "Tarvisio Boscoverde": null,
- "Tata": null,
- "Tatabanya": null,
- "Taufkirchen an der Pram": null,
- "Taulov st": null,
- "Taverne-Torricella": null,
- "Taxenbach-Rauris": null,
- "Salzburg Taxham Europark": null,
- "Tczew": null,
- "Tecknau": null,
- "Telfs-Pfaffenhofen": null,
- "Templeuve": null,
- "Tende(F)": null,
- "Tenneck": null,
- "Teplice v Cechach": null,
- "Terespol": null,
- "Brest(Gr)": null,
- "Terfens-Weer": null,
- "Tergnier": null,
- "Testelt": null,
- "Teting (Moselle)": null,
- "TGV Haute Picardie": null,
- "Thalwil": null,
- "Thionville": null,
- "Thun": null,
- "Thusis": null,
- "Tieffenbach-Struth": null,
- "Tienen": null,
- "Tilburg": null,
- "Tinglev st": null,
- "Tisis": null,
- "Nejdek-Tisova": null,
- "Tjaereborg st": null,
- "Tobel-Affeltrangen": null,
- "Toender st": null,
- "Toender Nord st": null,
- "Tommerup st": null,
- "Tongeren": null,
- "Torino Porta Susa": null,
- "Toul": null,
- "Toulon": null,
- "Toulouse-Matabiau": null,
- "Tourcoing": null,
- "Tournai": null,
- "Tournan": null,
- "Tovarnik": null,
- "Traun OÖ": null,
- "Trbovlje": null,
- "Trebusice": null,
- "Treibach-Althofen": null,
- "Trelleborg F": null,
- "Trento": null,
- "Treviso Centrale": null,
- "Trieben": null,
- "Trimmelkam": null,
- "Troisvierges": null,
- "Trooz": null,
- "Tschagguns": null,
- "Tulln a.d.Donau": null,
- "Tullnerfeld": null,
- "Tuplice": null,
- "Tuplice Debinka": null,
- "Turgi": null,
- "Tychy": null,
- "Uckange": null,
- "Udine": null,
- "Uitgeest": null,
- "Ulmerfeld-Hausmening": null,
- "Ulrichsbrücke-Füssen": null,
- "Unterberg-Stefansbrücke": null,
- "Unterterzen": null,
- "Unzmarkt": null,
- "Usti nad Labem-Strekov": null,
- "Usti nad Labem hl.n.": null,
- "Usti nad Labem zapad": null,
- "Utrecht Centraal": null,
- "Uttwil": null,
- "Vac": null,
- "Vaires Torcy": null,
- "Valby(Koebenhavn)": null,
- "Valdaora-Anterselva/Olang-Antholz": null,
- "Valdek": null,
- "Val-de-Reuil": null,
- "Valence Ville": null,
- "Valence TGV": null,
- "Valenciennes": null,
- "Valkenburg(NL)": null,
- "Vamdrup st": null,
- "Vandans": null,
- "Varangeville-St-Nicolas": null,
- "Varde st": null,
- "Varde Kaserne st": null,
- "Varde Vest st": null,
- "Varnsdorf stare nadr": null,
- "Varnsdorf": null,
- "Varnsdorf Pivovar Kocour": null,
- "Veenendaal-De Klomp": null,
- "Vejprty": null,
- "Velden am Wörther See": null,
- "Velke Zernoseky": null,
- "Velky Senov": null,
- "Velky Senov zast.": null,
- "Vendenheim": null,
- "Venezia Mestre": null,
- "Venezia Santa Lucia": null,
- "Venissieux": null,
- "Venlo": null,
- "Ventimiglia": null,
- "Vercelli": null,
- "Vernante": null,
- "Verneuil sur Avre": null,
- "Verneuil lEtang": null,
- "Vernon(Eure)": null,
- "Verona Porta Nuova": null,
- "Verviers Central": null,
- "Verviers-Palais": null,
- "Vesele pod Rabstejnem": null,
- "Wjasma": null,
- "Viby Sjaelland st": null,
- "Vicenza": null,
- "Vienne": null,
- "Vievola": null,
- "Vilemov u Sluknova": null,
- "Villabassa-Braies/Niederdorf-Prags": null,
- "Villach Hbf": null,
- "Villach Westbf": null,
- "Villars les Dombes": null,
- "Villedieu les Poeles": null,
- "Villers Cotterets": null,
- "Villiers-le-Bel-Gonesse": null,
- "Vils in Tirol": null,
- "Vilvoorde": null,
- "Vinkovci": null,
- "Vipiteno-Val di Vizze/Sterzing-Pfitsch": null,
- "Visby st": null,
- "Vise": null,
- "Visp": null,
- "Vitry le François Gare": null,
- "Vittel": null,
- "Vleuten": null,
- "Vlissingen": null,
- "Vöcklabruck": null,
- "Vöcklamarkt": null,
- "Voghera": null,
- "Vojens st": null,
- "Vojtanov": null,
- "Volders-Baumkirchen": null,
- "Völs": null,
- "Vordingborg st": null,
- "Vroegum st": null,
- "Vysoka Pec": null,
- "Wädenswil": null,
- "Wald am Schoberpass": null,
- "Walenstadt": null,
- "Walferdange": null,
- "Wallisellen": null,
- "Villach Warmbad": null,
- "Warszawa Centralna": null,
- "Warszawa Wschodnia": null,
- "Warszawa Zachodnia": null,
- "Wartberg/Krems": null,
- "Wartberg im Mürztal": null,
- "Wasserbillig": null,
- "Waterloo": null,
- "Watermael/Watermaal": null,
- "Wecker": null,
- "Weert": null,
- "Weesp": null,
- "Wegliniec": null,
- "Weinfelden": null,
- "Welkenraedt": null,
- "Wels Hbf": null,
- "Wendling b.Haag": null,
- "Werfen": null,
- "Wernstein": null,
- "Wespelaar-Tildonk": null,
- "Westendorf in Tirol": null,
- "Wettingen": null,
- "Wien Franz-Josefs-Bahnhof": null,
- "Flughafen Wien": null,
- "Wien Hbf": null,
- "Wien Hbf (Autoreisezuganlage)": null,
- "Wien Hütteldorf": null,
- "Wien Meidling": null,
- "Wien Mitte": null,
- "Wien Praterstern": null,
- "Wien Simmering": null,
- "Wien Westbahnhof": null,
- "Wiener Neustadt Hbf": null,
- "Wien Penzing": null,
- "Wil SG": null,
- "Wildon": null,
- "Wilwerwiltz": null,
- "Wilwisheim": null,
- "Windischgarsten": null,
- "Wingen-sur-Moder": null,
- "Winschoten": null,
- "Winterswijk": null,
- "Winterthur": null,
- "Wissembourg": null,
- "Witnica": null,
- "Wittenbach": null,
- "Wittring": null,
- "Woerden": null,
- "Wohlen AG": null,
- "Woippy": null,
- "Wolfstee": null,
- "Wolfurt": null,
- "Wolkersdorf im Weinviertel": null,
- "Wörgl Hbf": null,
- "Wormerveer": null,
- "Wroclaw Glowny": null,
- "Wroclaw Lesnica": null,
- "Wroclaw Nowy Dwor": null,
- "Ybbs a.d. Donau": null,
- "Yverdon-les-Bains": null,
- "Yves-Gomezee": null,
- "Zagan": null,
- "Zagorje": null,
- "Zagreb Glavni kolodvor": null,
- "Zandvoort aan Zee": null,
- "Zary": null,
- "Zasieki": null,
- "Zawiercie": null,
- "Zbaszynek": null,
- "Zebrzydowice": null,
- "Zedelgem": null,
- "Zeebrugge-Dorp": null,
- "Zelezna Ruda centrum": null,
- "Zelezna Ruda mesto": null,
- "Zell am See": null,
- "Zell am Ziller": null,
- "Zeltweg": null,
- "Zermatt": null,
- "Zetting": null,
- "Zevenaar": null,
- "Zgorzelec": null,
- "Zgorzelec Miasto": null,
- "Zichem": null,
- "Zidani Most": null,
- "Ziegelbrücke": null,
- "Zielona Gora Gl.": null,
- "Zirl": null,
- "Zirovice-Seniky": null,
- "Zizers": null,
- "Zofingen": null,
- "Zolder": null,
- "Zug(CH)": null,
- "Zuidbroek": null,
- "Zumarraga": null,
- "Zürich Enge": null,
- "Zürich Flughafen": null,
- "Zürich HB": null,
- "Zürich Wiedikon": null,
- "Zürich Wollishofen": null,
- "Zürich Altstetten": null,
- "Zürich Hardbrücke": null,
- "Zürich Oerlikon": null,
- "Opfikon": null,
- "Zürich Stadelhofen": null,
- "Zwijndrecht(NL)": null,
- "Zwolle": null
- }
- });
-});
diff --git a/public/static/js/autocomplete.min.js b/public/static/js/autocomplete.min.js
deleted file mode 100644
index d2b01c9..0000000
--- a/public/static/js/autocomplete.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-document.addEventListener("DOMContentLoaded",function(){var l=document.querySelectorAll(".autocomplete");M.Autocomplete.init(l,{minLength:3,limit:50,data:{"Aachen Hbf":null,"Aachen Schanz":null,"Aachen West":null,"Aachen West":null,"Aachen-Rothe Erde":null,"Aachen-Rothe Erde":null,"Aalen Hbf":null,Abenden:null,Abensberg:null,Achern:null,"Achern Stadt":null,Achim:null,Achkarren:null,Achmer:null,Achterwehr:null,Adelebsen:null,Adelschlag:null,"Adelsdorf(Mittelfr)":null,"Adelsheim Nord":null,"Adelsheim Ost":null,"Adorf(Erzgeb)":null,"Adorf(Vogtl)":null,Affaltrach:null,Agatharied:null,Agathenburg:null,Aglasterhausen:null,Aha:null,Ahaus:null,"Ahlbeck Grenze":null,"Ahlbeck Ostseetherme":null,"Ahlen(Westf)":null,Ahlhorn:null,Ahlten:null,"Ahnatal Casselbreite":null,"Ahnatal-Heckershausen":null,"Ahnatal-Weimar":null,"Ahrbrück":null,Ahrensburg:null,"Ahrensburg-Gartenholz":null,Ahrensfelde:null,"Ahrensfelde Friedhof":null,"Ahrensfelde Nord":null,Ahrweiler:null,"Ahrweiler Markt":null,"Aich(Niederbay)":null,Aichach:null,Aichstetten:null,Aindorf:null,Ainring:null,Albbruck:null,Albersdorf:null,"Albersweiler(Pfalz)":null,Albig:null,Albig:null,"Albisheim(Pfrimm)":null,Albrechtshaus:null,Albrechtshof:null,Albshausen:null,"Albsheim(Eis)":null,"Albstadt-Ebingen":null,"Albstadt-Ebingen West":null,"Albstadt-Laufen Ort":null,"Albstadt-Lautlingen":null,Aldekerk:null,"Aldingen(b Spaichingen)":null,Aletshausen:null,Alexisbad:null,"Alfeld(Leine)":null,"Alfter-Impekoven":null,"Alfter-Witterschlick":null,Algermissen:null,Aligse:null,"Allendorf(Dillkr)":null,Allensbach:null,"Allersberg(Rothsee)":null,Allmendingen:null,Alpen:null,Alpirsbach:null,"Alsdorf Poststraße":null,"Alsdorf(Westerw)":null,"Alsdorf-Annapark":null,"Alsdorf-Busch":null,"Alsdorf-Kellersberg":null,"Alsdorf-Mariadorf":null,Alsenz:null,"Alsfeld(Oberhess)":null,Alsheim:null,"Alt Hüttendorf":null,"Alt Rosenthal":null,Altbach:null,"Altdorf West":null,"Altdorf(Niederbay)":null,"Altdorf(b Nürnberg)":null,"Altdöbern":null,"Alte Veste":null,"Altefähr":null,"Altena(Westf)":null,Altenahr:null,"Altenau(Bay)":null,Altenbach:null,Altenbamberg:null,Altenbeken:null,Altenberge:null,Altenburg:null,Altenerding:null,"Altenfeld(Rhön)":null,Altenglan:null,"Altengörs":null,Altenhasungen:null,"Altenkirchen(Westerwald)":null,"Altenmarkt(Alz)":null,Altenseelbach:null,"Altenstadt(Hess)":null,"Altenstadt(Iller)":null,"Altenstadt(Waldnaab)":null,"Altenstadt-Höchst":null,"Altenstadt-Lindheim":null,Altentreptow:null,Altenwillershagen:null,Altersbach:null,"Altes Lager":null,"Altglashütten-Falkau":null,Althegnenberg:null,"Altheim(Hess)":null,Althof:null,"Altingen(Württ)":null,Altmittweida:null,Altmorschen:null,"Altmügeln":null,"Altomünster":null,"Altoschatz-Rosenthal":null,Altranft:null,Altshausen:null,"Altstädten(Allgäu)":null,Alttann:null,"Altötting":null,Alveslohe:null,"Alzenau Burg":null,"Alzenau Nord":null,"Alzenau(Unterfr)":null,Alzey:null,"Alzey Süd":null,"Alzey West":null,Amberg:null,Amerang:null,Ammern:null,Amorbach:null,Ampfing:null,Amsdorf:null,"Amstetten(W) Lokalbahn":null,"Amstetten(Württ)":null,Amtshainersdorf:null,Andernach:null,Angermund:null,"Angermünde":null,"Angern-Rogätz":null,Angersbach:null,Angersdorf:null,Anklam:null,"Annaberg-Buchholz Mitte":null,"Annaberg-Buchholz Süd":null,"Annaberg-Buchholz unterer Bf":null,Annaburg:null,"Annweiler am Trifels":null,"Annweiler-Sarnstall":null,Anrath:null,Ansbach:null,Antonsthal:null,Anwanden:null,Anzefahr:null,Anzenkirchen:null,Apensen:null,Apolda:null,Appenweier:null,Appenweier:null,Ardey:null,"Arensdorf(Köthen)":null,Arenshausen:null,"Arfurt(Lahn)":null,Armsheim:null,Arnbach:null,"Arnsberg(Westf)":null,Arnschwang:null,"Arnsdorf(Dresden)":null,"Arnstadt Hbf":null,"Arnstadt Süd":null,Arrach:null,Arsbeck:null,Artern:null,"Arzberg(Oberfr)":null,"Aschaffenburg Hbf":null,"Aschaffenburg Hochschule":null,"Aschaffenburg Süd":null,"Aschau(Chiemgau)":null,"Ascheberg(Holst)":null,"Ascheberg(Westf)":null,Aschendorf:null,Aschersleben:null,Ashausen:null,Asperg:null,Asselheim:null,"Assenheim(Oberhess)":null,Assmannshausen:null,Attendorn:null,"Attendorn-Hohen Hagen":null,"Au im Murgtal":null,"Au(Sieg)":null,"Aue(Sachs)":null,"Aue(Sachs) Erzgebirgsstadion":null,"Aue-Wingeshausen":null,"Auehütte":null,"Auerbach(V) ob Bf":null,"Auerbach(V) unt Bf":null,"Auerbach(Vogtl) Hp":null,"Auerbach(b Mosbach, Baden)":null,Auerstedt:null,"Aufhausen(Württ)":null,"Aufhausen(b Erding)":null,Auggen:null,"Augsburg Haunstetterstraße":null,"Augsburg Hbf":null,"Augsburg Messe":null,"Augsburg Morellstr.":null,"Augsburg-Hochzoll":null,"Augsburg-Hochzoll":null,"Augsburg-Oberhausen":null,Augustfehn:null,Aukrug:null,Aulendorf:null,Aumenau:null,"Aumühle":null,"Aumühle":null,"Auringen-Medenbach":null,"Auw an der Kyll":null,"Außenried":null,Aying:null,"Aßlar":null,"Aßling(Oberbay)":null,Baabe:null,Baalberge:null,"Baar-Ebenhausen":null,"Babenhausen Langstadt":null,"Babenhausen(Hess)":null,Babstadt:null,Bacharach:null,Bachern:null,Bachfeld:null,Bachheim:null,Backnang:null,"Bad Abbach":null,"Bad Aibling":null,"Bad Aibling Kurpark":null,"Bad Arolsen":null,"Bad Bellingen":null,"Bad Belzig":null,"Bad Bentheim":null,"Bad Bergzabern":null,"Bad Berka":null,"Bad Berka Zeughausplatz":null,"Bad Berleburg":null,"Bad Bevensen":null,"Bad Birnbach":null,"Bad Blankenburg(Thüringerw)":null,"Bad Bodendorf":null,"Bad Bodenteich":null,"Bad Brambach":null,"Bad Bramstedt":null,"Bad Bramstedt Kurhaus":null,"Bad Breisig":null,"Bad Camberg":null,"Bad Doberan":null,"Bad Doberan Goethestraße":null,"Bad Doberan Stadtmitte":null,"Bad Driburg(Westf)":null,"Bad Düben(Mulde)":null,"Bad Dürkheim":null,"Bad Dürkheim-Trift":null,"Bad Dürrenberg":null,"Bad Elster":null,"Bad Empfing":null,"Bad Ems":null,"Bad Ems West":null,"Bad Endorf":null,"Bad Fallingbostel":null,"Bad Freienwalde":null,"Bad Friedrichshall Hbf":null,"Bad Friedrichshall-Kochendorf":null,"Bad Gandersheim":null,"Bad Griesbach(Schwarzwald)":null,"Bad Grönenbach":null,"Bad Harzburg":null,"Bad Herrenalb":null,"Bad Hersfeld":null,"Bad Homburg":null,"Bad Honnef Stadtbahn":null,"Bad Honnef(Rhein)":null,"Bad Höhenstadt":null,"Bad Hönningen":null,"Bad Imnau":null,"Bad Karlshafen":null,"Bad Kissingen":null,"Bad Kleinen":null,"Bad Kohlgrub":null,"Bad Kohlgrub Kurhaus":null,"Bad Kreuznach":null,"Bad Krozingen":null,"Bad Krozingen Ost":null,"Bad König":null,"Bad König Zell":null,"Bad Kösen":null,"Bad Köstritz":null,"Bad Kötzting":null,"Bad Laasphe":null,"Bad Laasphe-Niederlaasphe":null,"Bad Langensalza":null,"Bad Lausick":null,"Bad Lauterberg im Harz Barbis":null,"Bad Liebenwerda":null,"Bad Liebenzell":null,"Bad Lobenstein":null,"Bad Malente-Gremsmühlen":null,"Bad Mergentheim":null,"Bad Münder(Deister)":null,"Bad Münster a Stein":null,"Bad Münstereifel":null,"Bad Münstereifel-Arloff":null,"Bad Münstereifel-Iversheim":null,"Bad Nauheim":null,"Bad Nenndorf":null,"Bad Neuenahr":null,"Bad Neustadt(Saale)":null,"Bad Niedernau":null,"Bad Oeynhausen":null,"Bad Oeynhausen Süd":null,"Bad Oldesloe":null,"Bad Peterstal":null,"Bad Pyrmont":null,"Bad Rappenau":null,"Bad Rappenau Kurpark":null,"Bad Reichenhall":null,"Bad Reichenhall-Kirchberg":null,"Bad Rodach":null,"Bad Rotenfels Bf":null,"Bad Rotenfels Schloss":null,"Bad Rotenfels Weinbrennerstraße":null,"Bad Saarow":null,"Bad Saarow Klinikum":null,"Bad Sachsa":null,"Bad Salzdetfurth":null,"Bad Salzdetfurth Solebad":null,"Bad Salzhausen":null,"Bad Salzschlirf":null,"Bad Salzuflen":null,"Bad Salzungen":null,"Bad Sassendorf":null,"Bad Saulgau":null,"Bad Schandau":null,"Bad Schlema":null,"Bad Schmiedeberg":null,"Bad Schmiedeberg Kurzentrum":null,"Bad Schmiedeberg Süd":null,"Bad Schussenried":null,"Bad Schwartau":null,"Bad Schönborn Süd":null,"Bad Schönborn-Kronau":null,"Bad Sebastiansweiler-Belsen":null,"Bad Segeberg":null,"Bad Sobernheim":null,"Bad Soden(Taunus)":null,"Bad Soden-Salmünster":null,"Bad Sooden-Allendorf":null,"Bad St Peter Süd":null,"Bad St Peter-Ording":null,"Bad Staffelstein":null,"Bad Steben":null,"Bad Suderode":null,"Bad Sulza":null,"Bad Sulza Nord":null,"Bad Säckingen":null,"Bad Teinach-Neubulach":null,"Bad Tölz":null,"Bad Tönisstein":null,"Bad Urach":null,"Bad Urach Ermstalklinik":null,"Bad Urach Wasserfall":null,"Bad Vilbel":null,"Bad Vilbel Süd":null,"Bad Vilbel-Gronau":null,"Bad Waldsee":null,"Bad Wildbad Bf":null,"Bad Wildbad Kurpark":null,"Bad Wildbad Nord":null,"Bad Wildbad Uhlandplatz":null,"Bad Wildungen":null,"Bad Wilsnack":null,"Bad Wimpfen":null,"Bad Wimpfen Im Tal":null,"Bad Wimpfen-Hohenstadt":null,"Bad Windsheim":null,"Bad Wurzach":null,"Bad Wörishofen":null,"Bad Zwischenahn":null,Baddeckenstedt:null,"Baden(Verden)":null,"Baden-Baden":null,"Baden-Baden Haueneberstein":null,"Baden-Baden Rebland":null,Bagenz:null,"Bahlingen Riedlen":null,"Bahlingen am Kaiserstuhl":null,"Bahnbrücken":null,Bahnsdorf:null,Baierbrunn:null,"Baiersbronn Bf":null,"Baiersbronn Schule":null,Baiersdorf:null,Baitz:null,Balbersdorf:null,Baldham:null,Balduinstein:null,Balgheim:null,"Balgstädt":null,"Balingen Süd":null,"Balingen(Württ)":null,"Ballstädt(Gotha)":null,Baltersweiler:null,Balve:null,Bamberg:null,Bammental:null,"Bannemin-Mölschow":null,"Bansin Seebad":null,Banteln:null,Bantorf:null,Barabein:null,Barbelroth:null,Bardowick:null,Bardowick:null,Bargstedt:null,Bargteheide:null,Barleben:null,"Barleber See":null,Barmstedt:null,"Barmstedt Brunnenstr":null,"Barnstorf(Han)":null,Barnten:null,Barrien:null,Barsinghausen:null,Barth:null,"Barthmühle":null,"Baruth(Mark)":null,"Basdahl Kluste":null,"Basdahl Kluste":null,Basdorf:null,"Basel Bad Bf":null,Bassum:null,"Batzenhäusle":null,Batzhausen:null,Bauerbach:null,Baumholder:null,Baunach:null,"Baunatal-Guntershausen":null,"Baunatal-Rengershausen":null,Bautzen:null,Bavendorf:null,Bayerbach:null,"Bayerisch Eisenstein":null,"Bayerisch Gmain":null,"Bayreuth Hbf":null,"Bayreuth-St Georgen":null,Bayrischzell:null,Bebitz:null,Bebra:null,"Bechstedt-Trippstein":null,"Beckingen(Saar)":null,"Bedburg(Erft)":null,"Bedburg-Hau":null,Beelen:null,"Beelitz Stadt":null,"Beelitz-Heilstätten":null,"Beerfelden Hetzbach":null,Beeskow:null,"Beetz-Sommerfeld":null,Behringersdorf:null,Beienheim:null,Beilrode:null,Beimerstetten:null,Beldorf:null,Belgershain:null,Belleben:null,Bellenberg:null,"Bellheim Am Mühlbuckel":null,"Bellheim Bf":null,Bempflingen:null,Benediktbeuern:null,Bengel:null,Benneckenstein:null,"Bennemühlen":null,Bennewitz:null,Bennigsen:null,"Benningen(Neckar)":null,Bennungen:null,Benshausen:null,Bensheim:null,"Bensheim-Auerbach":null,Bentwisch:null,Beratzhausen:null,Berbisdorf:null,"Berbisdorf Anbau":null,"Berchtesgaden Hbf":null,"Berg(Pfalz)":null,"Berga(Elster)":null,"Berga-Kelbra":null,"Bergen auf Rügen":null,"Bergen(Oberbay)":null,Bergenweiler:null,"Bergfelde(b Berlin)":null,"Berghausen Am Stadion":null,"Berghausen Pfinzbrücke":null,"Berghausen(Baden)":null,"Berghausen(Pfalz)":null,"Berghausen(b Wittgenstein)":null,"Bergheim(Erft)":null,"Bergisch Gladbach":null,Bergsdorf:null,Bergtheim:null,Bergwitz:null,"Beringen Bad Bf":null,Beringerfeld:null,Beringhausen:null,Beringstedt:null,"Berkenbrück":null,"Berlin Alexanderplatz":null,"Berlin Alexanderplatz (S)":null,"Berlin Alt-Reinickendorf":null,"Berlin Anhalter Bf":null,"Berlin Attilastr.":null,"Berlin Baumschulenweg":null,"Berlin Bellevue":null,"Berlin Betriebsbf Rummelsburg":null,"Berlin Betriebsbf Schöneweide":null,"Berlin Beusselstraße":null,"Berlin Bornholmer Str.":null,"Berlin Botanischer Garten":null,"Berlin Brandenburger Tor":null,"Berlin Buckower Chaussee":null,"Berlin Bundesplatz":null,"Berlin Eichborndamm":null,"Berlin Feuerbachstr.":null,"Berlin Frankfurter Allee":null,"Berlin Friedrichstraße":null,"Berlin Gehrenseestr.":null,"Berlin Gesundbrunnen":null,"Berlin Gesundbrunnen(S)":null,"Berlin Greifswalder Str":null,"Berlin Grünbergallee":null,"Berlin Hackescher Markt":null,"Berlin Hbf":null,"Berlin Hbf (S-Bahn)":null,"Berlin Hbf (tief)":null,"Berlin Heerstraße":null,"Berlin Heidelberger Platz":null,"Berlin Hermannstraße":null,"Berlin Hohenzollerndamm":null,"Berlin Humboldthain":null,"Berlin Innsbrucker Platz":null,"Berlin Jannowitzbrücke":null,"Berlin Julius-Leber-Brücke":null,"Berlin Jungfernheide":null,"Berlin Jungfernheide (S)":null,"Berlin Karl-Bonhoeffer-Nervenklinik":null,"Berlin Köllnische Heide":null,"Berlin Landsberger Allee":null,"Berlin Mehrower Allee":null,"Berlin Messe Nord/ICC (Witzleben)":null,"Berlin Messe Süd (Eichkamp)":null,"Berlin Mexikoplatz":null,"Berlin Nordbahnhof":null,"Berlin Nöldnerplatz":null,"Berlin Olympiastadion":null,"Berlin Oranienburger Straße":null,"Berlin Osdorfer Straße":null,"Berlin Ostbahnhof":null,"Berlin Ostkreuz":null,"Berlin Ostkreuz":null,"Berlin Ostkreuz (S)":null,"Berlin Ostkreuz (S)":null,"Berlin Ostkreuz (S)":null,"Berlin Plänterwald":null,"Berlin Poelchaustr.":null,"Berlin Potsdamer Platz":null,"Berlin Potsdamer Platz (S)":null,"Berlin Prenzlauer Allee":null,"Berlin Priesterweg":null,"Berlin Raoul-Wallenberg-Str.":null,"Berlin Rathaus Steglitz":null,"Berlin Savignyplatz":null,"Berlin Schichauweg":null,"Berlin Schönhauser Allee":null,"Berlin Sonnenallee":null,"Berlin Springpfuhl":null,"Berlin Storkower Str":null,"Berlin Sundgauer Str":null,"Berlin Südende":null,"Berlin Südkreuz":null,"Berlin Südkreuz (S)":null,"Berlin Südkreuz (S)":null,"Berlin Treptower Park":null,"Berlin Wannsee":null,"Berlin Wannsee":null,"Berlin Warschauer Straße":null,"Berlin Westend":null,"Berlin Westhafen":null,"Berlin Westkreuz":null,"Berlin Wollankstraße":null,"Berlin Wuhletal":null,"Berlin Yorckstr.(S1)":null,"Berlin Yorckstr.(S2)":null,"Berlin Zoologischer Garten":null,"Berlin Zoologischer Garten (S)":null,"Berlin-Adlershof":null,"Berlin-Altglienicke":null,"Berlin-Biesdorf":null,"Berlin-Blankenburg":null,"Berlin-Buch":null,"Berlin-Charlottenburg":null,"Berlin-Friedenau":null,"Berlin-Friedrichsfelde Ost":null,"Berlin-Friedrichshagen":null,"Berlin-Frohnau":null,"Berlin-Grunewald":null,"Berlin-Grünau":null,"Berlin-Halensee":null,"Berlin-Heiligensee":null,"Berlin-Hermsdorf":null,"Berlin-Hirschgarten":null,"Berlin-Hohenschönhausen":null,"Berlin-Karlshorst":null,"Berlin-Karow":null,"Berlin-Karow":null,"Berlin-Kaulsdorf":null,"Berlin-Köpenick":null,"Berlin-Lankwitz":null,"Berlin-Lichtenberg":null,"Berlin-Lichtenrade":null,"Berlin-Lichterfelde Ost":null,"Berlin-Lichterfelde Ost (S)":null,"Berlin-Lichterfelde Süd":null,"Berlin-Lichterfelde West":null,"Berlin-Mahlsdorf":null,"Berlin-Marienfelde":null,"Berlin-Marzahn":null,"Berlin-Neukölln":null,"Berlin-Neukölln":null,"Berlin-Nikolassee":null,"Berlin-Nikolassee":null,"Berlin-Oberspree":null,"Berlin-Pankow":null,"Berlin-Pankow-Heinersdorf":null,"Berlin-Pichelsberg":null,"Berlin-Rahnsdorf":null,"Berlin-Rummelsburg":null,"Berlin-Rummelsburg":null,"Berlin-Schlachtensee":null,"Berlin-Schulzendorf":null,"Berlin-Schöneberg":null,"Berlin-Schönefeld Flughafen":null,"Berlin-Schönefeld Flughafen":null,"Berlin-Schöneweide":null,"Berlin-Schöneweide (S)":null,"Berlin-Schönholz":null,"Berlin-Schönholz":null,"Berlin-Spandau":null,"Berlin-Spindlersfeld":null,"Berlin-Staaken":null,"Berlin-Stresow":null,"Berlin-Tegel (S)":null,"Berlin-Tempelhof":null,"Berlin-Tiergarten":null,"Berlin-Waidmannslust":null,"Berlin-Wartenberg":null,"Berlin-Wedding":null,"Berlin-Wilhelmshagen":null,"Berlin-Wilhelmsruh":null,"Berlin-Wittenau (Wilhelmsruher Damm)":null,"Berlin-Wuhlheide":null,"Berlin-Zehlendorf":null,"Bermatingen-Ahausen":null,"Bernau a Chiemsee":null,"Bernau(b Berlin)":null,"Bernau-Friedenstal":null,Bernburg:null,"Bernburg-Friedenshall":null,"Bernburg-Strenzfeld":null,"Bernburg-Waldau":null,"Bernburg-Waldau":null,Berne:null,Bernried:null,Bernterode:null,"Bersenbrück":null,"Berthelsdorf(Erzgeb)":null,"Berthelsdorf(Erzgebirge) Ort":null,Bertsdorf:null,Berzhahn:null,Besch:null,Besigheim:null,Besseringen:null,Bestensee:null,Bestwig:null,"Bettmannsäge":null,"Betzdorf(Sieg)":null,Beucha:null,Beuchow:null,Beuggen:null,"Beuna(Geiseltal)":null,Beuren:null,Beuron:null,Beutelsbach:null,Beutersitz:null,Bexbach:null,Beyendorf:null,"Bibelöd":null,"Biberach(Baden)":null,"Biberach(Riß)":null,"Biberach(Riß) Süd":null,Biblis:null,Bibra:null,Bichl:null,"Bickenbach(Bergstr)":null,Biebesheim:null,Biedenkopf:null,"Biedenkopf-Schulzentrum":null,Biederitz:null,"Bielefeld Hbf":null,"Bielefeld Ost":null,"Bielefeld-Senne":null,Biendorf:null,"Bienenbüttel":null,"Bienenmühle":null,Bierbach:null,"Bieren-Rödinghausen":null,Bieringen:null,"Biersdorf(Westerw)":null,"Biersdorf-Ort(Ww)":null,Biesenthal:null,Biessenhofen:null,Biessenhofen:null,"Bietigheim(Baden)":null,"Bietigheim-Bissingen":null,Bietingen:null,Bigge:null,Bildstock:null,Bilfingen:null,Billenhausen:null,Billerbeck:null,Binau:null,Bindfelde:null,Bindlach:null,"Bingen(Rhein) Hbf":null,"Bingen(Rhein) Stadt":null,"Bingen-Gaulsheim":null,Binolen:null,Binsfeld:null,"Binz LB":null,Binzen:null,Birach:null,Birkelbach:null,Birkenau:null,Birkenbringhausen:null,"Birkenfeld(Enz)":null,Birkengrund:null,Birkenmoor:null,Birkenstein:null,"Birkenwerder(b Berlin)":null,Birkungen:null,Birresborn:null,"Bischheim-Gersdorf":null,Bischofswerda:null,Bischofswiesen:null,Bischweier:null,Bisingen:null,Bissendorf:null,"Bitburg-Erdorf":null,Bittelbronn:null,Bitterfeld:null,Bitzfeld:null,"Blaibach(Oberpf)":null,"Blaichach(Allgäu)":null,Blankenbach:null,"Blankenberg(Meckl)":null,"Blankenberg(Sieg)":null,"Blankenburg(Harz)":null,"Blankenfelde(Teltow-Fläming)":null,"Blankenheim(Sangerhausen)":null,"Blankenheim(Wald)":null,Blankenloch:null,"Blankensee(Meckl)":null,"Blankenstein(Saale)":null,Blaubeuren:null,Blaufelden:null,Blaustein:null,"Blechhammer(Thür)":null,Bleibach:null,"Bleichenbach(Oberh)":null,"Bleicherode Ost":null,Blens:null,"Blieskastel-Lautzkirchen":null,Blindheim:null,"Blumberg(b Berlin)":null,"Blumberg-Rehhahn":null,"Blumberg-Riedöschingen":null,"Blumberg-Zollhaus":null,Blumenau:null,Blumenberg:null,Blumenhagen:null,Blumenkamp:null,"Blumenthal(Mark)":null,"Blönsdorf":null,Bobenheim:null,Bobingen:null,Bobitz:null,Bobstadt:null,Bocholt:null,"Bochum Hbf":null,"Bochum West":null,"Bochum-Dahlhausen":null,"Bochum-Ehrenfeld":null,"Bochum-Hamme":null,"Bochum-Langendreer":null,"Bochum-Langendreer":null,"Bochum-Langendreer West":null,"Bochum-Riemke":null,"Bockenheim-Kindenheim":null,"Bockum-Hövel":null,Bodelsberg:null,Bodelshausen:null,Bodenburg:null,Bodenfelde:null,Bodenheim:null,Bodenmais:null,Bodenrode:null,"Bodenwöhr Nord":null,Bogen:null,Bohmte:null,Boisheim:null,"Boizenburg(Elbe)":null,Bokholt:null,"Bondorf(b Herrenberg)":null,"Bonn Brühler Str.":null,"Bonn UN Campus":null,"Bonn Hbf":null,"Bonn Hbf (tief)":null,"Bonn Helmholtzstraße":null,"Bonn Heussallee/Museumsmeile":null,"Bonn Konrad-Adenauer-Platz":null,"Bonn Stadthaus":null,"Bonn-Bad Godesberg":null,"Bonn-Bad Godesberg Stadthalle":null,"Bonn-Beuel":null,"Bonn-Duisdorf":null,"Bonn-Endenich Nord":null,"Bonn-Mehlem":null,"Bonn-Oberkassel":null,"Bonn-Oberkassel Mitte":null,"Bonn-Ramersdorf":null,Bookholzberg:null,Boostedt:null,Bopfingen:null,"Boppard Hbf":null,"Boppard Süd":null,"Boppard-Bad Salzig":null,"Boppard-Buchholz":null,"Boppard-Fleckertshöhe":null,"Boppard-Hirzenach":null,Bordesholm:null,Borgeln:null,Borgholzhausen:null,Borgsdorf:null,"Bork(Westf)":null,"Borken(Hess)":null,"Borken(Westf)":null,Borkheide:null,"Borna(Leipzig)":null,"Borsdorf(Hess)":null,"Borsdorf(Sachs)":null,"Bottrop Hbf":null,"Bottrop Hbf":null,"Bottrop-Boy":null,"Bottrop-Vonderort":null,"Bous(Saar)":null,"Boxberg-Wölchingen":null,Brachbach:null,Brachelen:null,Brackwede:null,Brahlstorf:null,"Brake(Unterweser)":null,"Brake(b Bielefeld)":null,"Brake(b Bielefeld)":null,"Brakel(Höxter)":null,Bramsche:null,"Bramstedt(b Syke)":null,"Brand Tropical Islands":null,"Brandenburg Altstadt":null,"Brandenburg Hbf":null,Brandoberndorf:null,Brannenburg:null,Braubach:null,Braunsbedra:null,"Braunsbedra Ost":null,"Braunschweig Hbf":null,"Braunschweig-Gliesmarode":null,"Braunsdorf-Lichtenwalde":null,Breddin:null,Bredelar:null,Bredenbek:null,Bredstedt:null,Brehna:null,Breinig:null,Breisach:null,"Breitenbrunn(Erzg)":null,"Breitenbrunn(Schwab)":null,Breitendiel:null,Breitendorf:null,"Breitengüßbach":null,"Breitscheidt(Altenkirchen, Ww)":null,"Breitungen(Werra)":null,"Bremen Hbf":null,"Bremen Kreinsloger":null,"Bremen Mühlenstraße":null,"Bremen Neustadt":null,"Bremen Turnerstraße":null,"Bremen-Aumund":null,"Bremen-Blumenthal":null,"Bremen-Burg":null,"Bremen-Farge":null,"Bremen-Hemelingen":null,"Bremen-Lesum":null,"Bremen-Mahndorf":null,"Bremen-Oberneuland":null,"Bremen-Oslebshausen":null,"Bremen-Schönebeck":null,"Bremen-Sebaldsbrück":null,"Bremen-St Magnus":null,"Bremen-Vegesack":null,"Bremen-Walle":null,"Bremerhaven Hbf":null,"Bremerhaven-Lehe":null,"Bremerhaven-Wulsdorf":null,"Bremervörde":null,Brenk:null,"Brest-Aspe":null,Breternitz:null,Bretleben:null,Bretten:null,"Bretten Kupferhälde":null,"Bretten Rechberg":null,"Bretten Schulzentrum":null,"Bretten Stadtmitte":null,"Bretten Wannenweg":null,"Bretten-Ruit":null,Brettorf:null,"Bretzenheim(Nahe)":null,Bretzfeld:null,Breyell:null,Brieselang:null,"Briesen(Mark)":null,"Brigachtal Kirchdorf":null,"Brigachtal Klengen":null,"Brilon Stadt":null,"Brilon Wald":null,Britz:null,Brocken:null,"Brockhöfe":null,Broderstorf:null,Brohl:null,Brokstedt:null,"Bruchenbrücken":null,"Bruchhausen(b Ettlingen)":null,"Bruchköbel":null,"Bruchmühlbach-Miesau":null,"Bruchmühlen":null,Bruchsal:null,"Bruchsal Am Mantel":null,"Bruchsal Bildungszentrum":null,"Bruchsal Schlachthof":null,"Bruchsal Schloßgarten":null,"Bruchsal Sportzentrum":null,"Bruchsal Stegwiesen":null,"Bruchsal Tunnelstr.":null,Bruchweiler:null,Bruckberg:null,Brucken:null,"Bruckmühl":null,"Brunau-Packebusch":null,"Bräunlingen Bahnhof":null,"Bräunlingen Industriegebiet":null,"Brötzingen Mitte":null,"Brötzingen Sandweg":null,"Brötzingen Wohnlichstraße":null,"Brück(Mark)":null,"Brügge(Prign)":null,"Brügge(Westf)":null,"Brühl":null,"Brühl":null,"Brühl-Kierberg":null,Bubach:null,Bubenheim:null,Bubenreuth:null,"Buchbrunn-Mainstockheim":null,"Buchen Ost":null,"Buchen(Odenw)":null,"Buchenau(Lahn)":null,"Buchenau(Oberbay)":null,Buchenhain:null,Buchenhorst:null,"Buchholz(Baden)":null,"Buchholz(Nordheide)":null,"Buchholz(Zauche)":null,Buchloe:null,"Buckow(Beeskow)":null,Buddenhagen:null,Budenheim:null,Bufleben:null,Buggingen:null,Buir:null,Buldern:null,"Bullay(DB)":null,"Bundenthal-Rumbach":null,"Burbach Mitte":null,"Burbach(Kr Siegen)":null,"Burg Stargard(Meckl)":null,"Burg(Dillkr) Nord":null,"Burg(Dithm)":null,"Burg(Magdeburg)":null,"Burg-u. Nieder Gemünden":null,"Burgau(Schwab)":null,Burgbernheim:null,"Burgbernheim-Wildbad":null,Burgdorf:null,"Burghaun(Hünfeld)":null,Burghausen:null,Burgheim:null,"Burgholzhausen v d H":null,Burgkemnitz:null,Burgkirchen:null,Burgkunstadt:null,Burglauer:null,Burgsinn:null,"Burgstall(Murr)":null,"Burgstädt":null,Burgthann:null,Burgweiler:null,"Burhafe(Ostfriesl)":null,"Burkhardswalde-Maxen":null,Burkhardtsdorf:null,"Burkhardtsdorf Mitte":null,"Burkheim-Bischoffingen":null,Burladingen:null,"Burladingen West":null,Buschow:null,Busenbach:null,"Busenberg-Schindhard":null,Buttenheim:null,"Buttstädt":null,Butzbach:null,Buxtehude:null,"Bärenhecke-Johnsbach":null,"Bärenklau":null,"Bärenstein(Annaberg)":null,"Bärenstein(b Glashütte, Sachs)":null,"Bärnsdorf":null,"Bäumenheim":null,"Böbingen(Rems)":null,"Böblingen":null,"Böblingen Danziger Str":null,"Böblingen Heusteigstr":null,"Böblingen Südbf":null,"Böblingen Zimmerschlag":null,"Böbrach":null,"Böckingen Sonnenbrunnen":null,"Böckingen West":null,"Bödigheim":null,"Böhl-Iggelheim":null,"Böhlen Werke":null,"Böhlen(Leipzig)":null,"Böhmhof":null,"Böhringen-Rickelshausen":null,"Bölzke":null,"Bönen":null,"Bönningstedt":null,"Börnecke(Harz)":null,"Börßum":null,"Bösdorf(Sachs-Anh)":null,"Bösensell":null,"Bösperde":null,"Bötzingen":null,"Bötzingen Mühle":null,"Büchen":null,"Büchenbach":null,"Büches-Düdelsheim":null,"Bückeburg":null,"Büdingen(Oberhess)":null,"Büdingen(Westerw)":null,"Bühl(Baden)":null,"Bülzig":null,"Bünde(Westf)":null,"Bürgeln":null,"Bürstadt":null,"Bürstadt":null,"Büsenbachtal":null,"Büsum":null,"Büttgen":null,"Bützow":null,Cadenberge:null,Cadolzburg:null,Cainsdorf:null,"Calau(Nl)":null,"Calbe(Saale) Ost":null,"Calbe(Saale) Stadt":null,"Calbe(Saale) West":null,Calberlah:null,Caldern:null,"Calmbach Bahnhof":null,"Calmbach Süd":null,Calw:null,"Camburg(Saale)":null,"Cammin(Meckl)":null,"Capelle(Westf)":null,"Caputh Schwielowsee":null,"Caputh-Geltow":null,Casekow:null,"Castrop-Rauxel Hbf":null,"Castrop-Rauxel Süd":null,"Castrop-Rauxel Süd":null,"Castrop-Rauxel-Merklinde":null,Celle:null,"Cham(Oberpf)":null,Chamerau:null,"Chemnitz Alt Chemnitz Center":null,"Chemnitz Annenstraße":null,"Chemnitz Brückenstraße/Freie Presse":null,"Chemnitz Erdmannsdorfer Straße":null,"Chemnitz Erfenschlag":null,"Chemnitz Friedrichstraße":null,"Chemnitz Gustav-Freytag-Straße":null,"Chemnitz Hbf":null,"Chemnitz Hbf (Bahnhofstraße)":null,"Chemnitz Kinderwaldstätte":null,"Chemnitz Mitte":null,"Chemnitz Moritzhof":null,"Chemnitz Omnibusbahnhof":null,"Chemnitz Riemenschneiderstraße":null,"Chemnitz Roter Turm":null,"Chemnitz Rösslerstraße":null,"Chemnitz Scheffelstraße":null,"Chemnitz Schneeberger Straße":null,"Chemnitz Schule Altchemnitz":null,"Chemnitz Stefan-Heym-Platz":null,"Chemnitz Süd":null,"Chemnitz Süd":null,"Chemnitz Theaterplatz":null,"Chemnitz Treffurthstraße":null,"Chemnitz Uhlestraße":null,"Chemnitz Zentralhaltestelle":null,"Chemnitz-Altchemnitz":null,"Chemnitz-Borna Hp":null,"Chemnitz-Borna Hp":null,"Chemnitz-Harthau":null,"Chemnitz-Hilbersdorf":null,"Chemnitz-Reichenhain":null,"Chemnitz-Schönau":null,"Chemnitz-Siegmar":null,Chorin:null,Clarholz:null,Clausnitz:null,Cloppenburg:null,Coburg:null,"Coburg Nord":null,"Coburg-Neuses":null,"Cochem(Mosel)":null,"Coesfeld Schulzentrum":null,"Coesfeld(Westf)":null,Collenberg:null,Contwig:null,"Coppenbrügge":null,Coschen:null,Cossebaude:null,"Coswig(Anh)":null,"Coswig(b Dresden)":null,Cottbus:null,"Cottbus-Merzdorf":null,"Cottbus-Merzdorf":null,"Cottbus-Sandow":null,"Cottbus-Willmersdorf Nord":null,Crailsheim:null,Cranzahl:null,Creidlitz:null,"Creußen(Oberfr)":null,Crimmitschau:null,Crivitz:null,"Crossen Ort":null,"Crossen a d Elster":null,Cunnertswalde:null,Cursdorf:null,Cuxhaven:null,"Cölbe":null,Daaden:null,Dabendorf:null,"Dachau Bahnhof":null,"Dachau Stadt":null,Dachrieden:null,Dachwig:null,"Dagebüll Kirche":null,"Dagebüll Mole":null,Dahl:null,Dahlbruch:null,"Dahlem(Eifel)":null,"Dahlen(Sachs)":null,Dahlenburg:null,"Dahlerbrück":null,Dahlewitz:null,Dahn:null,"Dahn Süd":null,Dalheim:null,Dallau:null,"Dallgow-Döberitz":null,"Dannenberg Ost":null,"Dannenwalde(Gransee)":null,Darching:null,Darlingerode:null,"Darmstadt Hbf":null,"Darmstadt Nord":null,"Darmstadt Ost":null,"Darmstadt Süd":null,"Darmstadt TU-Lichtwiese":null,"Darmstadt-Arheilgen":null,"Darmstadt-Eberstadt":null,"Darmstadt-Kranichstein":null,"Darmstadt-Wixhausen":null,Dasing:null,"Dattenfeld(Sieg)":null,Dauenhof:null,Daufenbach:null,Dausenau:null,Davensberg:null,Dedenhausen:null,"Dedensen-Gümmer":null,Dedinghausen:null,"Deezbüll":null,"Deggendorf Hbf":null,Deidesheim:null,"Deining(Oberpf)":null,Deinste:null,Deinste:null,Deisenhofen:null,"Deißlingen Mitte":null,"Delitzsch ob Bf":null,"Delitzsch unt Bf":null,Dellfeld:null,"Dellfeld Ort":null,Delmenhorst:null,"Demitz-Thumitz":null,Demker:null,Demmin:null,Densborn:null,Denzlingen:null,Dernau:null,"Dernbach(Westerw)":null,"Derneburg(Han)":null,"Dessau Adria":null,"Dessau Hbf":null,"Dessau Süd":null,"Dessau-Alten":null,"Dessau-Mosigkau":null,"Dessau-Waldersee":null,Detmold:null,"Dettelbach Bahnhof":null,Dettenhausen:null,"Dettingen Freibad":null,"Dettingen Gsaidt":null,"Dettingen Lehen":null,"Dettingen(Main)":null,"Dettingen(Teck)":null,"Dettingen-Mitte":null,Dettum:null,"Deuben(Zeitz)":null,Deuerling:null,Deuten:null,Deutzen:null,Diebach:null,Dieburg:null,Diedelsheim:null,"Diedorf(Schwab)":null,Dienheim:null,Diepholz:null,Dieringhausen:null,Dieskau:null,Dietersheim:null,Dietmannsried:null,Dietzelbach:null,"Dietzenbach Bahnhof":null,"Dietzenbach Mitte":null,"Dietzenbach-Steinberg":null,Dietzhausen:null,Diez:null,"Diez Ost":null,"Dießen":null,Dillbrecht:null,Dillenburg:null,"Dillingen(Donau)":null,"Dillingen(Saar)":null,Dingden:null,Dingolfing:null,"Dinkelsbühl Bf":null,Dinkelscherben:null,Dinslaken:null,Dippoldiswalde:null,Dirmingen:null,"Dissen-Bad Rothenfelde":null,Distelhausen:null,Ditfurt:null,Dittersbach:null,Dittersdorf:null,Dittigheim:null,Ditzingen:null,"Doberlug-Kirchhain":null,"Doberlug-Kirchhain":null,"Doberschütz":null,Dodendorf:null,Dodenhof:null,Dogern:null,"Dohna(Sachs)":null,Dollbergen:null,Dollern:null,Dollnstein:null,"Dombühl":null,"Domnitz(Saalkr)":null,"Domsühl":null,Donaueschingen:null,"Donaueschingen Allmendshofen":null,"Donaueschingen Aufen":null,"Donaueschingen Grüningen":null,"Donaueschingen Mitte/Siedlung":null,"Donauwörth":null,"Dorf Mecklenburg":null,Dorfchemnitz:null,"Dorfen Bahnhof":null,Dorfmark:null,Dorfprozelten:null,"Dorheim(Wetterau)":null,Dormagen:null,"Dormagen Chempark":null,"Dormagen Chempark":null,"Dornburg(Saale)":null,Dornstetten:null,Dorsten:null,Dortelweil:null,"Dortmund Hbf":null,"Dortmund Hbf":null,"Dortmund Knappschaftskrankenhaus":null,"Dortmund Möllerbrücke":null,"Dortmund Signal Iduna Park":null,"Dortmund Stadthaus":null,"Dortmund Tierpark":null,"Dortmund Universität":null,"Dortmund West":null,"Dortmund-Aplerbeck":null,"Dortmund-Aplerbeck Süd":null,"Dortmund-Asseln Mitte":null,"Dortmund-Barop":null,"Dortmund-Brackel":null,"Dortmund-Bövinghausen":null,"Dortmund-Derne":null,"Dortmund-Dorstfeld":null,"Dortmund-Dorstfeld":null,"Dortmund-Dorstfeld":null,"Dortmund-Dorstfeld Süd":null,"Dortmund-Germania":null,"Dortmund-Huckarde":null,"Dortmund-Huckarde Nord":null,"Dortmund-Hörde":null,"Dortmund-Kirchderne":null,"Dortmund-Kirchhörde":null,"Dortmund-Kley":null,"Dortmund-Kruckel":null,"Dortmund-Kurl":null,"Dortmund-Körne":null,"Dortmund-Körne West":null,"Dortmund-Löttringhausen":null,"Dortmund-Lütgendortmund":null,"Dortmund-Lütgendortmund Nord":null,"Dortmund-Marten":null,"Dortmund-Marten Süd":null,"Dortmund-Mengede":null,"Dortmund-Nette/Oestrich":null,"Dortmund-Oespel":null,"Dortmund-Rahm":null,"Dortmund-Scharnhorst":null,"Dortmund-Somborn":null,"Dortmund-Sölde":null,"Dortmund-Westerfilde":null,"Dortmund-Wickede":null,"Dortmund-Wickede West":null,"Dortmund-Wischlingen":null,"Dorum(Weserm)":null,"Dossow(Prign)":null,Dottenheim:null,"Dotternhausen-Dormettingen":null,Drahnsdorf:null,Drahtzug:null,Drebkau:null,"Drei Annen Hohne":null,"Dreieich-Buchschlag":null,"Dreieich-Buchschlag":null,"Dreieich-Dreieichenhain":null,"Dreieich-Götzenhain":null,"Dreieich-Offenthal":null,"Dreieich-Sprendlingen":null,"Dreieich-Weibelfeld":null,Dreikirchen:null,"Dreileben-Drackenstedt":null,Drensteinfurt:null,"Dresden Bischofsplatz":null,"Dresden Flughafen":null,"Dresden Freiberger Straße":null,"Dresden Grenzstraße":null,"Dresden Hbf":null,"Dresden Industriegelände":null,"Dresden Mitte":null,"Dresden-Cotta":null,"Dresden-Dobritz":null,"Dresden-Friedrichstadt":null,"Dresden-Kemnitz":null,"Dresden-Klotzsche":null,"Dresden-Neustadt":null,"Dresden-Niedersedlitz":null,"Dresden-Pieschen":null,"Dresden-Plauen":null,"Dresden-Reick":null,"Dresden-Stetzsch":null,"Dresden-Strehlen":null,"Dresden-Trachau":null,"Dresden-Zschachwitz":null,Dreye:null,"Drohndorf-Mehringen":null,Ducherow:null,Duckterath:null,Dudweiler:null,"Duisburg Entenfang":null,"Duisburg Hbf":null,"Duisburg-Bissingheim":null,"Duisburg-Buchholz":null,"Duisburg-Großenbaum":null,"Duisburg-Hochfeld Süd":null,"Duisburg-Hochfeld Süd":null,"Duisburg-Meiderich Ost":null,"Duisburg-Meiderich Süd":null,"Duisburg-Obermeiderich":null,"Duisburg-Rahm":null,"Duisburg-Ruhrort":null,"Duisburg-Schlenk":null,"Duisburg-Wedau":null,"Duisburg-Wedau":null,Durach:null,Durmersheim:null,"Durmersheim Nord":null,"Dutenhofen(Wetzlar)":null,"Dußlingen":null,"Döbeln Hbf":null,"Döberitz":null,"Döggingen":null,"Döhlau":null,"Döllstädt":null,"Dörfles-Esbach":null,"Dörpen":null,"Dörrberg":null,"Dörverden":null,"Dülken":null,"Dülmen":null,"Dülmen":null,"Düren":null,"Düren":null,"Düren Annakirmesplatz":null,"Düren Im Großen Tal":null,
-"Düren Renkerstraße":null,"Düren-Kuhbrücke":null,"Düren-Lendersdorf":null,"Dürrenbüchig":null,"Dürrenwaid Bahnhof":null,"Dürrnhaar":null,"Dürrröhrsdorf":null,"Düsseldorf Flughafen":null,"Düsseldorf Flughafen":null,"Düsseldorf Flughafen Terminal":null,"Düsseldorf Friedrichstadt":null,"Düsseldorf Hbf":null,"Düsseldorf Volksgarten":null,"Düsseldorf Völklinger Str.":null,"Düsseldorf Wehrhahn":null,"Düsseldorf-Benrath":null,"Düsseldorf-Bilk":null,"Düsseldorf-Derendorf":null,"Düsseldorf-Derendorf":null,"Düsseldorf-Eller":null,"Düsseldorf-Eller Mitte":null,"Düsseldorf-Eller Süd":null,"Düsseldorf-Flingern":null,"Düsseldorf-Garath":null,"Düsseldorf-Gerresheim":null,"Düsseldorf-Hamm":null,"Düsseldorf-Hellerhof":null,"Düsseldorf-Oberbilk":null,"Düsseldorf-Rath":null,"Düsseldorf-Rath Mitte":null,"Düsseldorf-Reisholz":null,"Düsseldorf-Unterrath":null,"Düsseldorf-Zoo":null,"Dütschow":null,"Ebelsbach-Eltmann":null,"Ebenhausen(Unterfr)":null,"Ebenhausen-Schäftlarn":null,Ebenhofen:null,Ebensfeld:null,Eberbach:null,Ebermannstadt:null,Ebermergen:null,Ebern:null,"Ebersbach(Fils)":null,"Ebersbach(Sachs)":null,"Ebersberg(Oberbay)":null,Ebersbrunn:null,"Ebersdorf(b Coburg)":null,"Eberswalde Hbf":null,Ebertsheim:null,Ebing:null,Ebringen:null,"Ebstorf(Uelzen)":null,Echem:null,Eching:null,Echterdingen:null,Echzell:null,Eckardtsleben:null,"Eckartsberga(Thür)":null,"Eckartshausen-Ilshofen":null,"Eckernförde":null,"Eckersmühlen":null,Eddersheim:null,Edelfingen:null,Edenkoben:null,"Edermünde-Grifte":null,"Edesheim(Pfalz)":null,"Ediger-Eller":null,"Edingen(Wetzlar)":null,"Edle Krone":null,Edling:null,"Effelder(Thür)":null,Effolderbach:null,"Efringen-Kirchen":null,Egelsbach:null,Egersdorf:null,"Egestorf(Deister)":null,Eggenfelden:null,"Eggenfelden Mitte":null,"Eggenstein Bf":null,Eggersdorf:null,Eggesin:null,Eggingen:null,Egglkofen:null,"Eggmühl":null,Eggolsheim:null,Eglharting:null,Egling:null,"Ehingen(Donau)":null,Ehlenbruch:null,Ehlershausen:null,"Ehningen(b Böblingen)":null,Ehr:null,Ehrang:null,"Ehrang Ort":null,Ehringen:null,"Ehringhausen(Kr Lippstadt)":null,"Ehringshausen(Kr Wetzlar)":null,"Ehringshausen(Oberhess)":null,Eibau:null,"Eichen(Kr Siegen)":null,"Eichenau(Oberbay)":null,Eichenberg:null,Eichenzell:null,Eichhagen:null,Eicholzheim:null,"Eichstedt(Altm)":null,"Eichstetten am Kaiserstuhl":null,"Eichstätt Bahnhof":null,"Eichstätt Stadt":null,Eichwalde:null,Eickendorf:null,Eilenburg:null,"Eilenburg Ost":null,"Eilenburg Ost":null,Eilendorf:null,"Eilsleben(b Magdeburg)":null,Eilvese:null,Eimeldingen:null,"Einbeck Salzderhelden":null,Einfeld:null,Einsiedel:null,"Einsiedel Hp Gymnasium":null,Einsiedlerhof:null,"Einöd(Saar)":null,Eisenach:null,"Eisenach Opelwerke Hp":null,"Eisenach West":null,"Eisenbach-Matzenbach":null,"Eisenberg(Pfalz)":null,"Eisenberg(Pfalz)":null,"Eisenberg(Pfalz)":null,Eisenheim:null,"Eisenhüttenstadt":null,"Eisenärzt":null,"Eiserfeld(Sieg)":null,Eisfeld:null,"Eisfelder Talmühle":null,"Eislingen(Fils)":null,Eiswoog:null,Eitensheim:null,Eitorf:null,Elend:null,"Elfershausen-Trimberg":null,Elgersburg:null,Ellefeld:null,Ellental:null,Ellerau:null,Ellhofen:null,"Ellingen(Bay)":null,Ellrich:null,Ellwangen:null,Ellzee:null,Elmenhorst:null,Elmshorn:null,Elpersheim:null,Elsfleth:null,Elsholz:null,"Elsnigk(Anh)":null,Elstal:null,"Elster(Elbe)":null,Elsterberg:null,"Elsterberg-Kunstseidenwerk":null,Elsterwerda:null,"Elsterwerda-Biehla":null,Eltersdorf:null,Eltville:null,Elxleben:null,"Elz(Limburg/Lahn)":null,"Elz(Limburg/Lahn) Süd":null,Elzach:null,"Elze(Han)":null,"Emden Außenhafen":null,"Emden Hbf":null,Emmelshausen:null,Emmendingen:null,Emmerich:null,Emmerke:null,Emmerthal:null,"Empel-Rees":null,Empelde:null,Emsdetten:null,Emskirchen:null,Endersbach:null,"Endingen am Kaiserstuhl":null,"Endingen(Württ)":null,Engeln:null,Engelskirchen:null,Engen:null,Engers:null,Engertsham:null,Engstingen:null,Engstlatt:null,Enkenbach:null,Ennepetal:null,"Ensdorf(Saar)":null,Enspel:null,Entringen:null,Enzberg:null,Enzisweiler:null,"Epe(Westf)":null,Eppelborn:null,"Eppelsheim(Rheinhess)":null,Eppertshausen:null,Eppingen:null,"Eppingen West":null,Eppstein:null,"Eppstein-Bremthal":null,"Erbach(Odenw)":null,"Erbach(Odenw) Nord":null,"Erbach(Rheingau)":null,"Erbach(Württ)":null,Erdeborn:null,Erding:null,Erdmannhausen:null,"Erdmannsdorf-Augustusburg":null,Erdweg:null,Erftstadt:null,"Erfurt Hbf":null,"Erfurt Nord":null,"Erfurt Ost":null,"Erfurt-Bischleben":null,"Erfurt-Gispersleben":null,Ergenzingen:null,Ergoldsbach:null,Ergste:null,Eriskirch:null,Erkelenz:null,Erkersreuth:null,Erkner:null,Erkrath:null,"Erkrath-Nord":null,Erla:null,"Erlabrunn(Erzgeb)":null,Erlangen:null,"Erlangen Paul-Gossen-Straße":null,"Erlangen-Bruck":null,"Erlau(Sachs)":null,"Erlenbach(Main)":null,"Erndtebrück":null,Ernsgaden:null,Ernsthausen:null,"Ernstthal am Rennsteig":null,"Erpel(Rhein)":null,Erpolzheim:null,Ersingen:null,"Ersingen West":null,Erzhausen:null,"Erzingen(Baden)":null,"Erzingen(Württ)":null,Eschborn:null,"Eschborn Süd":null,Eschede:null,Eschelbronn:null,"Eschenau(Mittelfr)":null,"Eschenau(b Heilbronn)":null,"Eschenbach(b Markt Erlbach)":null,Eschenlohe:null,"Escherndorf-Vogelburg":null,Eschhofen:null,Eschwege:null,"Eschwege-Niederhone":null,"Eschweiler Hbf":null,"Eschweiler Talbahnhof":null,"Eschweiler-Nothberg":null,"Eschweiler-St.Jöris":null,"Eschweiler-Weisweiler":null,"Eschweiler-West":null,"Esens(Ostfriesl)":null,Espelkamp:null,"Espenau-Mönchehof":null,"Essen Hbf":null,"Essen Stadtwald":null,"Essen Süd":null,"Essen West":null,"Essen(Oldb)":null,"Essen-Altenessen":null,"Essen-Bergeborbeck":null,"Essen-Borbeck":null,"Essen-Borbeck Süd":null,"Essen-Dellwig":null,"Essen-Dellwig Ost":null,"Essen-Eiberg":null,"Essen-Frohnhausen":null,"Essen-Gerschede":null,"Essen-Holthausen":null,"Essen-Horst":null,"Essen-Hügel":null,"Essen-Kray Nord":null,"Essen-Kray Süd":null,"Essen-Kupferdreh":null,"Essen-Steele":null,"Essen-Steele Ost":null,"Essen-Werden":null,"Essen-Zollverein Nord":null,"Essen-Überruhr":null,"Esslingen(Neckar)":null,"Esslingen-Mettingen":null,"Esslingen-Zell":null,Esting:null,Etelsen:null,Ettenhausen:null,Etterzhausen:null,"Ettlingen Stadt":null,"Ettlingen West":null,Etzbach:null,Etzelwang:null,Etzenbach:null,Etzenricht:null,Etzenrot:null,Etzleben:null,Eubigheim:null,Euerdorf:null,Euskirchen:null,"Euskirchen Zuckerfabrik":null,"Euskirchen-Großbüllesheim":null,"Euskirchen-Kreuzweingarten":null,"Euskirchen-Kuchenheim":null,"Euskirchen-Stotzheim":null,Eutin:null,"Eutingen im Gäu":null,"Eutingen(Baden)":null,Eutzsch:null,Eyach:null,Eystrup:null,"Eßleben":null,"Fachingen(Lahn)":null,Fahrenkrug:null,Fahrnau:null,"Falkenau(Sachs)Hp":null,"Falkenau(Sachs)Süd":null,"Falkenberg(Elster)":null,"Falkenberg(Elster)":null,"Falkenberg(Elster)":null,"Falkenberg(Mark)":null,"Falkenhagen Gewerbepark Prignitz":null,Falkensee:null,"Falkenstein(Vogtl)":null,Fallersleben:null,Fangschleuse:null,Farchant:null,Fasanenpark:null,"Faulbach(Main)":null,Faurndau:null,Favoritepark:null,"Fehmarn-Burg":null,Feilitzsch:null,Feldafing:null,"Feldberg-Bärental":null,Felde:null,Feldhausen:null,"Feldkirchen(b München)":null,Fellbach:null,"Felsberg-Altenbrunslar":null,"Felsberg-Gensungen":null,"Felsberg-Wolfershausen":null,"Ferch-Lienewitz":null,Ferdinandshof:null,Fermerswalde:null,"Ferndorf(Siegen)":null,Feucht:null,"Feucht Ost":null,"Feucht-Moosbach":null,"Feuchtwangen Bf":null,Feudingen:null,Fichtenberg:null,Filderstadt:null,Filsen:null,Finkenheerd:null,Finkenkrug:null,Finnentrop:null,"Finningerstraße":null,"Finsterwalde(Niederlausitz)":null,"Fischbach(Nürnberg)":null,"Fischbach-Camphausen":null,"Fischbach-Weierbach":null,Fischbachau:null,Fischen:null,Fischhaus:null,"Fischhausen-Neuhaus":null,Fladungen:null,Flechtingen:null,Fleetmark:null,Flehingen:null,Flensburg:null,Flieden:null,Flintbek:null,Flintsbach:null,Flomersheim:null,"Floßmühle":null,"Flöha":null,"Flöha-Plaue":null,"Flörsheim(Main)":null,Fohrde:null,"Forbach(Schwarzw)":null,"Forchheim(Oberfr)":null,"Forchheim(b Karlsruhe)":null,Fornsbach:null,"Forst(Lausitz)":null,Forsthaus:null,Forsting:null,Forth:null,Frahelsbruck:null,"Frankenberg(Eder)":null,"Frankenberg(Sachs)":null,"Frankenberg(Sachs) Süd":null,"Frankenberg-Goßberg":null,"Frankenberg-Viermünden":null,"Frankenstein(Pfalz)":null,"Frankenstein(Sachs)":null,"Frankenthal Hbf":null,"Frankenthal Süd":null,"Frankfurt Hbf (tief)":null,"Frankfurt am Main - Stadion":null,"Frankfurt(M) Flughafen Fernbf":null,"Frankfurt(M) Flughafen Regionalbf":null,"Frankfurt(M)Galluswarte":null,"Frankfurt(M)Hauptwache":null,"Frankfurt(M)Konstablerwache":null,"Frankfurt(M)Lokalbahnhof":null,"Frankfurt(M)Mühlberg":null,"Frankfurt(M)Ostendstraße":null,"Frankfurt(M)Stresemannallee":null,"Frankfurt(M)Taunusanlage":null,"Frankfurt(Main)Hbf":null,"Frankfurt(Main)Messe":null,"Frankfurt(Main)Ost":null,"Frankfurt(Main)Süd":null,"Frankfurt(Main)West":null,"Frankfurt(Oder)":null,"Frankfurt(Oder)-Neuberesinchen":null,"Frankfurt(Oder)-Rosengarten":null,"Frankfurt-Berkersheim":null,"Frankfurt-Eschersheim":null,"Frankfurt-Frankfurter Berg":null,"Frankfurt-Griesheim":null,"Frankfurt-Höchst":null,"Frankfurt-Höchst Farbwerke":null,"Frankfurt-Louisa":null,"Frankfurt-Louisa":null,"Frankfurt-Mainkur":null,"Frankfurt-Nied":null,"Frankfurt-Niederrad":null,"Frankfurt-Niederrad":null,"Frankfurt-Rödelheim":null,"Frankfurt-Sindlingen":null,"Frankfurt-Sindlingen":null,"Frankfurt-Sossenheim":null,"Frankfurt-Unterliederbach":null,"Frankfurt-Zeilsheim":null,Frankleben:null,"Frauenalb-Schielberg":null,Frauenau:null,Frauenhain:null,"Frechen-Königsdorf":null,Freckleben:null,"Freden(Leine)":null,"Fredersdorf(b Berlin)":null,"Freiberg(Neckar)":null,"Freiberg(Sachs)":null,"Freiburg Klinikum":null,"Freiburg Messe/Universität":null,"Freiburg West":null,"Freiburg(Breisgau) Hbf":null,"Freiburg-Herdern":null,"Freiburg-Littenweiler":null,"Freiburg-St Georgen":null,"Freiburg-Wiehre":null,"Freiburg-Zähringen":null,Freienohl:null,Freienorla:null,Freihalden:null,Freihung:null,"Freihöls":null,Freilassing:null,"Freilassing-Hofham":null,"Freimersheim(Rheinh)":null,Freinsheim:null,Freising:null,"Freital-Coßmannsdorf":null,"Freital-Deuben":null,"Freital-Hainsberg":null,"Freital-Hainsberg":null,"Freital-Hainsberg West":null,"Freital-Potschappel":null,Frellstedt:null,Frelsdorf:null,"Fremdingen Bf":null,Fremersdorf:null,Frenz:null,Fresenburg:null,Fretzdorf:null,"Freudenberg-Kirschfurt":null,"Freudenstadt Hbf":null,"Freudenstadt Industriegebiet":null,"Freudenstadt Schulzentrum":null,"Freudenstadt Stadt":null,"Freusburg Siedlung":null,"Freyburg(Unstrut)":null,"Freyung Bf":null,Frickenhausen:null,"Frickenhausen Kelterstraße":null,Frickhofen:null,"Fridingen(b Tuttlingen)":null,Fridolfing:null,"Friedberg Süd":null,"Friedberg(Augsburg)":null,"Friedberg(Hess)":null,Friedelhausen:null,"Friedensdorf(Lahn)":null,"Friedersdorf(Königs Wusterhausen)":null,"Friedewald(Kr Dresden) Bad":null,"Friedewald(Kr Dresden)Hp":null,"Friedland(Han)":null,"Friedrich Wilhelmshütte":null,Friedrichroda:null,"Friedrichsdorf(Taunus)":null,"Friedrichsfeld(Niederrhein)":null,Friedrichsgabe:null,"Friedrichshafen Flughafen":null,"Friedrichshafen Hafen":null,"Friedrichshafen Landratsamt":null,"Friedrichshafen Ost":null,"Friedrichshafen Stadt":null,"Friedrichshafen-Fischbach":null,"Friedrichshafen-Kluftern":null,"Friedrichshafen-Manzell":null,"Friedrichshöhe":null,Friedrichsruh:null,"Friedrichsruhe(Meck)":null,Friedrichssegen:null,Friedrichstadt:null,"Friedrichstal b Freudenstadt":null,"Friedrichstal(Baden)":null,"Friedrichsthal(Saar)":null,"Friedrichsthal(Saar) Mitte":null,"Friedrichsthal(b Bayreuth)":null,"Friesack(Mark)":null,"Friesenheim(Baden)":null,Frimmersdorf:null,Fritzlar:null,Frohburg:null,Frommern:null,"Fronhausen(Lahn)":null,Frose:null,"Frömern":null,"Fröndenberg":null,"Fröttstädt":null,Fulda:null,"Fuldatal-Ihringshausen":null,Furschenbach:null,"Furth i Wald":null,"Furth(b Deisenhofen)":null,"Fährbrücke":null,"Föhren":null,"Förbau":null,"Förderstedt":null,"Förtha(Eisenach)":null,"Förtschendorf":null,"Fürfurt":null,"Fürstenberg(Havel)":null,"Fürsteneck":null,"Fürstenfeldbruck":null,"Fürstenwald":null,"Fürstenwalde Süd":null,"Fürstenwalde(Spree)":null,"Fürstenzell":null,"Fürth Westvorstadt":null,"Fürth(Bay)Hbf":null,"Fürth(Odenw)":null,"Fürth-Burgfarrnbach":null,"Fürth-Dambach":null,"Fürth-Unterfarrnbach":null,"Fürth-Unterfarrnbach":null,"Fürth-Unterfürberg":null,"Füssen":null,Gablingen:null,Gadebusch:null,"Gaggenau Bf":null,"Gaggenau Mercedes-Benz Werk":null,"Gaildorf West":null,Gaimersheim:null,"Gaißach":null,Galgenschanze:null,"Gamburg(Tauber)":null,Gammertingen:null,"Gammertingen Europastraße":null,Ganderkesee:null,"Gangloffsömmern":null,Garbeck:null,Garbenteich:null,"Garching(Alz)":null,Gardelegen:null,Garding:null,Garftitz:null,"Garmisch-Partenkirchen":null,"Garmisch-Partenkirchen Hausberg":null,"Gars(Inn)":null,Gatersleben:null,"Gau Algesheim":null,"Gau Bickelheim":null,"Gaubüttelbrunn":null,Gausbach:null,Gauselfingen:null,Gauting:null,"Gebra(Hainleite)":null,Geeste:null,Geestenseth:null,Geestgottberg:null,Gehlberg:null,Geigant:null,Geilenkirchen:null,Geilenkirchen:null,Geilhausen:null,"Geiselhöring":null,Geisenbrunn:null,Geisenhausen:null,Geisenheim:null,Geising:null,Geisingen:null,"Geisingen-Aulfingen":null,"Geisingen-Hausen":null,"Geisingen-Kirchen":null,"Geisingen-Leipferdingen":null,"Geislingen(Steige)":null,"Geislingen(Steige)West":null,Geitau:null,Geithain:null,Gelbensande:null,Geldern:null,Gelnhausen:null,"Gelsenkirchen Hbf":null,"Gelsenkirchen Zoo":null,"Gelsenkirchen-Buer Nord":null,"Gelsenkirchen-Buer Süd":null,"Gelsenkirchen-Hassel":null,"Gelsenkirchen-Rotthausen":null,Geltendorf:null,Gemmingen:null,"Gemmingen West":null,"Gemünden(Main)":null,Genderkingen:null,Gendorf:null,Gengenbach:null,Gennweiler:null,"Gensingen-Horrweiler":null,Genthin:null,"Georgensgmünd":null,"Gera Hbf":null,"Gera Ost":null,"Gera Süd":null,"Gera-Langenberg":null,"Gera-Liebschwitz":null,"Gera-Zwötzen":null,Geraberg:null,Geradstetten:null,Gerhausen:null,Gerichshain:null,Gerlachsheim:null,Gerlenhofen:null,"Germering-Unterpfaffenhofen":null,"Germersheim Bahnhof":null,"Germersheim Mitte/Rhein":null,"Germersheim Süd/Nolte":null,Gernlinden:null,"Gernrode(Harz)":null,"Gernrode-Niederorschel":null,"Gernsbach Bf":null,"Gernsbach Mitte":null,Gernsheim:null,Geroldshausen:null,Gerolstein:null,"Gersdorf(Görlitz)":null,"Gersfeld(Rhön)":null,Gerstetten:null,Gersthofen:null,Gerstungen:null,Gertenbach:null,Gerwisch:null,Geseke:null,Gessertshausen:null,"Gettenau-Bingenheim":null,Gettorf:null,"Gevelsberg Hbf":null,"Gevelsberg West":null,"Gevelsberg-Kipp":null,"Gevelsberg-Knapp":null,"Giengen(Brenz)":null,Giersleben:null,"Gießen":null,"Gießen Erdkauter Weg":null,"Gießen Licher Str":null,"Gießen Oswaldsgarten":null,Gifhorn:null,"Gifhorn Stadt":null,"Gilching-Argelsried":null,"Gingen(Fils)":null,Girod:null,"Gittelde/Bad Grund(Harz)":null,"Gladbeck Ost":null,"Gladbeck West":null,"Gladbeck-Zweckel":null,"Glan-Münchweiler":null,Glanzstoffwerke:null,"Glashütte(Sachs)":null,"Glaubitz(Riesa)":null,"Glauburg-Glauberg":null,"Glauburg-Stockheim":null,"Glauchau(Sachs)":null,"Glauchau-Schönbörnchen":null,Glesch:null,"Glossen (b Oschatz)":null,"Glöwen":null,"Glückauf":null,"Glückstadt":null,"Gmund(Tegernsee)":null,Gnadau:null,Gnarrenburg:null,"Gnarrenburg Nord":null,Gnevkow:null,Goch:null,"Gochsheim(Baden)":null,Godelheim:null,Godramstein:null,Gokels:null,"Goldbeck(Osterburg)":null,"Goldberg(Württ)":null,"Goldenstedt(Oldb)":null,Goldhausen:null,"Goldshöfe":null,"Gollmitz(Niederlausitz)":null,Golm:null,"Golzow(Eberswalde)":null,"Golzow(Oderbruch)":null,"Golßen(Niederlausitz)":null,Gomadingen:null,Gommern:null,"Gondelsheim Schloßstadion":null,"Gondelsheim(Baden)":null,Gorgast:null,Gosberg:null,Goslar:null,Gotha:null,"Gotha Ost":null,Gottenheim:null,Gotteszell:null,Gottmadingen:null,"Goßdorf-Kohlmühle":null,"Goßfelden":null,"Goßmannsdorf":null,"Graal-Müritz":null,"Graal-Müritz Koppelweg":null,"Graben(Lechfeld)Gewerbepark":null,"Graben-Neudorf":null,"Graben-Neudorf Nord":null,"Grabow(Meckl)":null,Grafenaschau:null,Grafenau:null,Grafenwiesen:null,"Grafing Bahnhof":null,"Grafing Stadt":null,"Grafling-Arzting":null,Grafrath:null,Grambow:null,Gransee:null,"Grauschwitz Flocke":null,Grebenstein:null,Greifswald:null,"Greifswald Süd":null,Greiz:null,"Greiz-Dölau":null,Grenzach:null,Greppin:null,"Greußen":null,Greven:null,Grevenbroich:null,"Grevesmühlen":null,"Grieben(Meckl)":null,Griebo:null,Griefstedt:null,Griefstedt:null,"Griesen(Oberbay)":null,"Grießen(Baden)":null,"Grimma ob Bf":null,Grimmen:null,Grimmenthal:null,Grobau:null,Grombach:null,"Gronau(Westf)":null,Gronsdorf:null,"Groß Ammensleben":null,"Groß Brütz":null,"Groß Düngen":null,"Groß Gerau":null,"Groß Gerau-Dornberg":null,"Groß Gerau-Dornheim":null,"Groß Karben":null,"Groß Kiesow":null,"Groß Kreutz":null,"Groß Köris":null,"Groß Laasch":null,"Groß Lüsewitz":null,"Groß Pankow":null,"Groß Quassow":null,"Groß Rohrheim":null,"Groß Schwaß":null,"Groß Schönebeck":null,"Groß-Umstadt Klein-Umstadt":null,"Groß-Umstadt Mitte":null,"Groß-Umstadt Wiebelsbach":null,"Großarmschlag":null,"Großauheim(Kr Hanau)":null,"Großbeeren":null,"Großbothen":null,"Großburgwedel":null,"Großdeuben":null,"Großen Buseck":null,"Großen Linden":null,"Großenaspe":null,"Großenbrode":null,"Großengottern":null,"Großenhain Cottb Bf":null,"Großenkneten":null,"Großenlüder":null,"Großfurra":null,"Großgeschaidt":null,"Großharthau":null,"Großhelfendorf":null,"Großheringen":null,"Großhesselohe Isartalbf":null,"Großkarolinenfeld":null,"Großkorbetha":null,"Großkrotzenburg":null,"Großkugel":null,"Großlehna":null,"Großneuhausen":null,"Großpösna":null,"Großrudestedt":null,"Großräschen":null,"Großröhrsdorf":null,"Großschwabhausen":null,"Großschönau(Sachs)":null,"Großsteinberg":null,"Großwalbur":null,"Großwudicke":null,"Grub am Forst":null,"Grub(Oberbay)":null,"Grub(Oberpf)":null,Gruiten:null,Gruiten:null,Grunbach:null,"Grunow(Niederlausitz)":null,"Gräfelfing":null,"Gräfenberg":null,"Gräfendorf":null,"Gräfenhainichen":null,"Gräfenroda":null,"Gräfentonna":null,"Gräveneck":null,"Grävenwiesbach":null,"Gröbenzell":null,"Gröbers":null,"Gröditz(Riesa)":null,"Grötzingen":null,"Grötzingen Krappmühlenweg":null,"Grötzingen Oberausstraße":null,"Grüna(Sachs)Hp":null,"Grünbach(Vogtl)":null,"Grünberg(Oberhess)":null,"Grünebach Ort":null,"Grünebacherhütte":null,"Grüneberg":null,"Grünhainichen-Borstendorf":null,"Grünsfeld":null,"Grünstadt":null,"Grünstadt Nord":null,"Grüntal-Wittlensweiler":null,"Gstadt(Wanderbahn)":null,Guben:null,Gummersbach:null,"Gumpenried-Asbach":null,"Gundelfingen(Bay)":null,"Gundelfingen(Breisgau)":null,Gundelsdorf:null,Gundelshausen:null,"Gundelsheim(Neckar)":null,"Gundersheim(Rheinhess)":null,Guntersblum:null,Gunzenhausen:null,Gussenstadt:null,Gustorf:null,"Gutach Freilichtmuseum":null,"Gutach(Breisgau)":null,"Gutenfürst":null,Guthmannshausen:null,Guxhagen:null,"Gärtringen":null,"Gäufelden":null,"Göbelnrod":null,"Göhrde":null,"Göhren(Rügen)":null,"Göllheim-Dreisen":null,"Gölshausen":null,"Gölshausen Industriegebiet":null,"Göppingen":null,"Görden":null,"Görlitz":null,"Görlitz-Rauschwalde":null,"Görlitz-Weinhübel":null,"Görsbach":null,"Görschnitz":null,"Göttingen":null,"Götz":null,"Gößnitz":null,"Gültstein":null,"Gündlkofen":null,"Güntersberge":null,"Günzach":null,"Günzburg":null,"Güsen(b Genthin)":null,"Güsten":null,"Güstrow":null,"Güterglück":null,"Güterglück":null,"Gütersloh Hbf":null,Haan:null,Haar:null,Haarhausen:null,Hachenburg:null,Hadamar:null,Hademarschen:null,Hadmersleben:null,Haffkrug:null,"Hagebök":null,Hagelstadt:null,"Hagen Hbf":null,"Hagen(Han)":null,"Hagen-Heubing":null,"Hagen-Oberhagen":null,"Hagen-Vorhalle":null,"Hagen-Wehringhausen":null,"Hagen-Westerbauer":null,Hagenbach:null,"Hagenbüchach":null,"Hagenbüchach":null,"Hagenow Land":null,"Hagenow Stadt":null,Hagenwerder:null,"Haidenaab-Göppmannsbühl":null,Haidkapelle:null,Haiger:null,"Haiger Obertor":null,Haigerloch:null,Haigerloch:null,"Hailer-Meerholz":null,"Hainburg Hainstadt":null,Hainewalde:null,Hainichen:null,"Hainstadt(Baden)":null,"Haitz-Höchst":null,Halbe:null,Halberstadt:null,"Halberstadt Oststr":null,"Halberstadt-Spiegelsberge":null,Halbmeil:null,Haldensleben:null,"Haldern(Rheinl)":null,Halen:null,Halfing:null,Hallbergmoos:null,"Halle Dessauer Brücke":null,"Halle Messe":null,"Halle Rosengarten":null,"Halle Steintorbrücke":null,"Halle Südstadt":null,"Halle Wohnstadt Nord":null,"Halle Zoo":null,"Halle Zscherbener Straße":null,"Halle(Saale)Hbf":null,"Halle(Saale)Hbf Gl. 13a":null,"Halle(Saale)Hbf Gl. 13a":null,"Halle(W) Gerry-Weber-Stadion":null,"Halle(Westf)":null,"Halle-Ammendorf":null,"Halle-Ammendorf":null,"Halle-Neustadt":null,"Halle-Nietleben":null,"Halle-Silberhöhe":null,"Halle-Trotha":null,"Halle-Trotha":null,"Hallstadt(b Bamberg)":null,Halstenbek:null,"Haltern am See":null,Haltingen:null,"Hamburg Airport":null,"Hamburg Alte Wöhr":null,"Hamburg Berliner Tor":null,"Hamburg Berliner Tor":null,"Hamburg Berliner Tor":null,"Hamburg Billwerder-Moorfleet":null,"Hamburg Burgwedel":null,"Hamburg Dammtor":null,"Hamburg Dammtor":null,"Hamburg Diebsteich":null,"Hamburg Elbgaustraße":null,"Hamburg Friedrichsberg":null,"Hamburg Hasselbrook":null,"Hamburg Hasselbrook":null,"Hamburg Hbf":null,"Hamburg Hbf (S-Bahn)":null,"Hamburg Hochkamp":null,"Hamburg Hoheneichen":null,"Hamburg Holstenstraße":null,"Hamburg Jungfernstieg":null,"Hamburg Klein Flottbek":null,"Hamburg Kornweg(Klein Borstel)":null,"Hamburg Königstraße":null,"Hamburg Landungsbrücken":null,"Hamburg Landungsbrücken":null,"Hamburg Landwehr":null,"Hamburg Mittlerer Landweg":null,"Hamburg Neuwiedenthal":null,"Hamburg Reeperbahn":null,"Hamburg Rübenkamp":null,"Hamburg Stadthausbrücke":null,"Hamburg Wandsbeker Chaussee":null,"Hamburg-Allermöhe":null,"Hamburg-Altona":null,"Hamburg-Altona(S)":null,"Hamburg-Bahrenfeld":null,"Hamburg-Barmbek":null,"Hamburg-Bergedorf":null,"Hamburg-Bergedorf":null,"Hamburg-Blankenese":null,"Hamburg-Eidelstedt":null,"Hamburg-Eidelstedt":null,"Hamburg-Eidelstedt":null,"Hamburg-Eidelstedt Zentrum":null,"Hamburg-Fischbek":null,"Hamburg-Hammerbrook":null,"Hamburg-Harburg":null,"Hamburg-Harburg Rathaus":null,"Hamburg-Harburg(S)":null,"Hamburg-Heimfeld":null,"Hamburg-Hörgensweg":null,"Hamburg-Iserbrook":null,"Hamburg-Langenfelde":null,"Hamburg-Nettelnburg":null,"Hamburg-Neugraben":null,"Hamburg-Neugraben":null,"Hamburg-Ohlsdorf":null,"Hamburg-Othmarschen":null,"Hamburg-Poppenbüttel":null,"Hamburg-Rahlstedt":null,"Hamburg-Rissen":null,"Hamburg-Rissen":null,"Hamburg-Rothenburgsort":null,"Hamburg-Rothenburgsort":null,"Hamburg-Schnelsen":null,"Hamburg-Stellingen":null,"Hamburg-Stellingen":null,"Hamburg-Sternschanze":null,"Hamburg-Sülldorf":null,"Hamburg-Tiefstack":null,"Hamburg-Tonndorf":null,"Hamburg-Veddel":null,"Hamburg-Wandsbek":null,"Hamburg-Wellingsbüttel":null,"Hamburg-Wilhelmsburg":null,Hameln:null,"Hamm(Westf)":null,Hammah:null,Hammelburg:null,"Hammelburg Ost":null,Hammelspring:null,Hammerau:null,Hammerstein:null,Hammerunterwiesenthal:null,Hamminkeln:null,"Hanau Hbf":null,"Hanau Hbf":null,"Hanau Hbf":null,"Hanau Klein-Auheim":null,"Hanau Nord":null,"Hanau West":null,"Hanau-Wilhelmsbad":null,Handeloh:null,Hanfertal:null,"Hangelar Mitte":null,Hangelsberg:null,"Hann Münden":null,"Hannover Anderten-Misburg":null,"Hannover Bismarckstr.":null,"Hannover Flughafen":null,"Hannover Hbf":null,"Hannover Karl-Wiechert-Allee":null,"Hannover Messe/Laatzen":null,"Hannover-Bornum":null,"Hannover-Kleefeld":null,"Hannover-Ledeburg":null,"Hannover-Leinhausen":null,"Hannover-Linden/Fischerhof":null,"Hannover-Nordstadt":null,"Hannover-Vinnhorst":null,Happurg:null,Harblek:null,"Harburg(Schwab)":null,Hardegsen:null,Hardhof:null,"Haren(Ems)":null,Harra:null,"Harra Nord":null,"Harras(Thür)":null,Harsdorf:null,Harsefeld:null,Harsum:null,Hartenstein:null,Hartershofen:null,Harthaus:null,"Hartmannmühle":null,Hartmannshof:null,"Harxheim-Zell":null,Harzgerode:null,Hasbergen:null,Haselbrunn:null,Haslach:null,"Hasloch(Main)":null,Hasloh:null,Haslohfurth:null,Haspelmoor:null,"Hassel(Saar)":null,Hasselborn:null,Hasselfelde:null,Haste:null,Hattenheim:null,"Hattersheim(Main)":null,Hattert:null,"Hattingen(R) Mitte":null,"Hattingen(Ruhr)":null,Hattorf:null,Hatzenport:null,Haubersbronn:null,"Haubersbronn Mitte":null,"Hauenstein Mitte":null,"Hauenstein(Pfalz)":null,"Haunetal-Neukirchen":null,Haupeltshofen:null,Hauptstuhl:null,Hausach:null,"Hausen (b Düren)":null,"Hausen i Tal":null,"Hausen(Eichsfeld)":null,"Hausen(Schwab)":null,"Hausen(Taunus)":null,"Hausen-Raitbach":null,"Hausen-Starzeln":null,Hausham:null,Havixbeck:null,"Haßfurt":null,"Haßloch(Pfalz)":null,"Haßmersheim":null,Hebertsfelden:null,Hebertshausen:null,Hechingen:null,"Hechingen Landesbahn":null,Hechthausen:null,"Heddesheim/Hirschberg":null,"Hedemünden":null,Hedersdorf:null,"Hedersleben-Wedderstedt":null,Heessen:null,Heggen:null,Hegne:null,"Heide(Holst)":null,"Heidelberg Hbf":null,"Heidelberg Orthopädie":null,"Heidelberg-Altstadt":null,"Heidelberg-Kirchheim/Rohrbach":null,"Heidelberg-Pfaffengrund/Wieblingen":null,"Heidelberg-Schlierbach/Ziegelhausen":null,"Heidelberg-Weststadt/Südstadt":null,Heidelsheim:null,"Heidelsheim Nord":null,Heidenau:null,"Heidenau Süd":null,"Heidenau-Großsedlitz":null,Heidenheim:null,"Heidenheim Voithwerk":null,"Heidenheim-Mergelstetten":null,"Heidenheim-Schnaitheim":null,"Heidesheim(Rheinhess)":null,Heidkrug:null,"Heigenbrücken":null,"Heilbad Heiligenstadt":null,"Heilbr.-Böckingen Berufsschulzentrum":null,"Heilbronn Finanzamt":null,"Heilbronn Friedensplatz":null,"Heilbronn Hans-Rießer-Straße":null,"Heilbronn Harmonie":null,"Heilbronn Harmonie/Hafenmarktpassage":null,"Heilbronn Harmonie/Kunsthalle":null,"Heilbronn Hauptbahnhof/Willy-Brandt-Pl.":null,"Heilbronn Hbf":null,"Heilbronn Industrieplatz":null,"Heilbronn Kaufland":null,"Heilbronn Neckar-Turm/K.-S.-Pl":null,"Heilbronn Pfühlpark":null,"Heilbronn Rathaus":null,"Heilbronn Sülmertor":null,"Heilbronn Technisches Schulzentrum":null,"Heilbronn Theater":null,"Heilbronn Trappensee":null,Heiligendamm:null,Heiligengrabe:null,"Heiligenstatt(Obb)":null,"Heiligenstein(Pfalz)":null,Heilsbronn:null,"Heimbach (Eifel)":null,"Heimbach(Nahe)":null,"Heimbach(Nahe)Ort":null,Heimenkirch:null,Heimersheim:null,Heimstetten:null,Heinebach:null,"Heinsberg Kreishaus":null,"Heinsberg(Rheinl)":null,"Heinsberg-Dremmen":null,"Heinsberg-Horst":null,"Heinsberg-Oberbruch":null,"Heinsberg-Porselen":null,"Heinsberg-Randerath":null,Heinschenwalde:null,Heinzenhausen:null,Heitersheim:null,Heldrungen:null,Helenesee:null,Helmbrechts:null,"Helmsdorf(Pirna)":null,Helmsheim:null,"Helmstadt(Baden)":null,Helmstedt:null,Helpup:null,Hemmerde:null,"Hemmersdorf(Saar)":null,Hemmingen:null,Hemmoor:null,Hemsbach:null,Henfenfeld:null,"Hennef im Siegbogen":null,"Hennef(Sieg)":null,Hennen:null,"Hennersdorf(Sachs)":null,"Hennigsdorf(b Berlin)":null,"Henstedt-Ulzburg":null,"Heppenheim(Bergstr)":null,Herbertingen:null,"Herbertingen Ort":null,Herbertshofen:null,Herblingen:null,"Herbolzheim(Breisg)":null,"Herbolzheim(Jagst)":null,"Herborn(Dillkr)":null,Herbrechtingen:null,Herchen:null,Herdecke:null,Herdorf:null,Herford:null,Hergatz:null,Hergershausen:null,"Heringen(Helme)":null,"Heringsdorf Neuhof":null,"Herlasgrün":null,"Herleshausen Hp":null,Hermaringen:null,Hermentingen:null,"Hermsdorf(Dresden)":null,"Hermsdorf-Klosterlausnitz":null,Herne:null,"Herne-Börnig":null,Heroldsberg:null,"Heroldsberg Nord":null,Herrath:null,Herrenberg:null,Herrenberg:null,"Herrenberg Zwerchweg":null,Herrensee:null,Herrlingen:null,"Herrlishöfen":null,Herrnburg:null,Herrsching:null,"Hersbruck(l Pegnitz)":null,"Hersbruck(r Pegnitz)":null,"Herten(Baden)":null,"Hervest-Dorsten":null,"Herxheim am Berg":null,"Herzberg Schloß":null,"Herzberg(Elster)":null,"Herzberg(Harz)":null,"Herzberg(Mark)":null,Herzebrock:null,Herzhorn:null,Herzogenrath:null,"Herzogenrath-Alt-Merkstein":null,"Herzogenrath-August-Schmidt-Platz":null,Hesedorf:null,Heselbach:null,Hesepe:null,Hesseln:null,"Hesseneck Kailbach":null,"Hesseneck Schöllenbach":null,"Hessisch Oldendorf":null,Hetschburg:null,Hettenhausen:null,"Hettingen(Hohenz)":null,Hettstedt:null,"Hetzdorf(Flöhatal)":null,Hetzerath:null,"Heudeber-Danstedt":null,Heufeld:null,"Heufeldmühle":null,Heusenstamm:null,"Hiddenhausen-Schweicheln":null,Hilchenbach:null,Hildburghausen:null,Hilden:null,"Hilden Süd":null,"Hildesheim Hbf":null,"Hildesheim Ost":null,"Hillnhütten":null,Hilpertsau:null,Hilpoltstein:null,Hilter:null,Himmelpforten:null,Himmelreich:null,Himmelstadt:null,Hinrichssegen:null,Hinterweidenthal:null,"Hinterweidenthal Ort":null,"Hinterweidenthal Ost":null,Hinterzarten:null,Hirsau:null,Hirschaid:null,Hirschfelde:null,Hirschfelden:null,"Hirschhorn(Neckar)":null,"Hirschhorn(Pfalz)":null,Hittfeld:null,Hitzacker:null,Hochdahl:null,"Hochdahl-Millrath":null,"Hochdorf(b Horb)":null,"Hochhausen(Tauber)":null,"Hochheim(Main)":null,Hochneukirch:null,Hochspeyer:null,"Hochstadt-Marktzeuln":null,Hochstetten:null,"Hochstetten Grenzstraße":null,"Hochstetten(Nahe)":null,"Hochstätten(Pfalz)":null,Hochwang:null,Hockenheim:null,Hockeroda:null,Hodenhagen:null,"Hof Hbf":null,"Hof(Münstertal)":null,"Hof-Neuhof":null,Hofeld:null,"Hofen(b Aalen)":null,Hoffenheim:null,Hoffnungsthal:null,Hofgeismar:null,"Hofgeismar-Hümme":null,"Hofheim (Ried)":null,"Hofheim(Taunus)":null,Hohegrete:null,"Hohen Neuendorf West":null,"Hohen Neuendorf(b Berlin)":null,Hohenbrunn:null,Hohendorf:null,"Hohenebra Ort":null,Hoheneggelsen:null,Hohenfichte:null,Hohenleipisch:null,Hohenleuben:null,Hohenlimburg:null,"Hohenpeißenberg":null,Hohenroda:null,"Hohenschäftlarn":null,"Hohenstadt(Mittelfr)":null,"Hohenstein-Ernstthal":null,"Hohensülzen":null,Hohenthurm:null,Hohenwarth:null,"Hohenwarth Campingplatz":null,Hohenwestedt:null,Hohenwulsch:null,"Hohndorf Mitte":null,"Holdorf(Meckl)":null,"Holdorf(Oldb)":null,"Holm-Seppensen":null,Holstentherme:null,"Holtensen/Linderte":null,Holthusen:null,"Holzdorf(Elster)":null,"Holzdorf(b Weimar)":null,"Holzgerlingen Bf":null,"Holzgerlingen Buch":null,"Holzgerlingen Nord":null,Holzhau:null,"Holzhau Skilift":null,"Holzhausen(Kr Siegen)":null,"Holzhausen-Heddinghausen":null,"Holzheim(b Neuss)":null,Holzkirchen:null,Holzminden:null,Holzwickede:null,"Homburg(Saar)Hbf":null,Honrath:null,"Hopfgarten(Sachs)":null,"Hopfgarten(Weimar)":null,Hoppecke:null,"Hoppegarten(Mark)":null,"Hoppegarten(Mark)":null,Hoppingen:null,"Hoppstädten(Nahe)":null,Horb:null,"Horb-Heiligenfeld":null,Horka:null,"Horn-Bad Meinberg":null,"Hornberg(Schwarzw)":null,Horneburg:null,Hornstorf:null,Horrem:null,"Horst(Holst)":null,Hosena:null,Hoyerswerda:null,Hoykenkamp:null,Hubacker:null,"Hubertushöhe":null,"Huchem-Stammeln":null,Huckstorf:null,Hude:null,Hufschlag:null,Huglfing:null,Hugstetten:null,Hulb:null,Hummelberg:null,"Hundsgrün":null,Hundstadt:null,Hungen:null,Huntlosen:null,Husby:null,Husum:null,Huttenheim:null,Huzenbach:null,"Hähnichen":null,"Hähnlein-Alsbach":null,"Hämelerwald":null,"Hämerten":null,"Häuserhof":null,"Höchst Hetschbach":null,"Höchst Mümling-Grumbach":null,"Höchst(Odenw)":null,"Höchstädt(Donau)":null,"Höfen(Enz) Bf":null,"Höfen(Enz) Nord":null,"Höfingen":null,"Höhenkirchen-Siegertsbrunn":null,"Höhmühlbach":null,"Höllenthal":null,"Höllriegelskreuth":null,"Höpfling":null,"Hörden":null,"Hörlkofen":null,"Hörpolding":null,"Hörschel Hp":null,"Hörselgau":null,"Hörstel":null,"Hörstmar(Lippe)":null,"Hösbach":null,"Hösel":null,"Hövelhof":null,"Hövelriege":null,"Höxter Rathaus":null,"Hückelhoven-Baal":null,"Hüffenhardt":null,"Hüfingen Mitte":null,"Hünfeld":null,"Hürth-Kalscheuren":null,"Hütten":null,"Hüttenbusch":null,"Hüttengrund":null,"Hüttingen":null,Ibach:null,"Ibbenbüren":null,"Ibbenbüren-Esch":null,"Ibbenbüren-Laggenbeck":null,Ichenhausen:null,Icking:null,"Idar-Oberstein":null,"Idstein(Taunus)":null,Iffeldorf:null,Igel:null,Igensdorf:null,Igersheim:null,Ihringen:null,Ilberstedt:null,Ilfeld:null,"Ilfeld Bad":null,"Ilfeld Neanderklinik":null,"Ilfeld Schreiberwiese":null,Illertissen:null,Illesheim:null,"Illingen(Saar)":null,"Illingen(Württ)":null,Ilmenau:null,"Ilmenau Bad":null,"Ilmenau Pörlitzer Höhe":null,"Ilmenau-Roda":null,Ilsenburg:null,
-Immelborn:null,Immendingen:null,"Immendingen Mitte":null,"Immendingen Zimmern":null,Immenhausen:null,Immenreuth:null,"Immensen-Arpke":null,Immenstadt:null,Imsweiler:null,Ingelbach:null,Ingelheim:null,"Ingolstadt Hbf":null,"Ingolstadt Nord":null,Inningen:null,"Inselstadt Malchow":null,Insheim:null,Iphofen:null,Ipsheim:null,"Irfersgrün":null,Irrenlohe:null,Iserlohn:null,Iserlohnerheide:null,Isernhagen:null,Ismaning:null,Ispringen:null,"Isselhorst-Avenwedde":null,Istein:null,"Ittersbach Bahnhof":null,"Ittersbach Rathaus":null,Ittling:null,Ittlingen:null,Itzehoe:null,Itzelberg:null,"Jabel(Meckl)":null,"Jacobsdorf(Mark)":null,"Jagdschloß":null,Jagstzell:null,"Jahnsdorf(Erzgeb)":null,"Jakobwüllesheim":null,Jarrenwisch:null,Jasnitz:null,Jatznick:null,"Jeber-Bergfrieden":null,Jechtingen:null,Jeeser:null,"Jena Paradies":null,"Jena Saalbf":null,"Jena West":null,"Jena-Göschwitz":null,"Jena-Zwätzen":null,"Jesewitz(Leipzig)":null,"Jessen(Elster)":null,Jettenbach:null,Jettingen:null,Jever:null,"Jeßnitz(Anh)":null,Joachimsthal:null,"Joachimsthal Kaiserbahnhof":null,Jocketa:null,"Jockgrim Bf":null,Johanngeorgenstadt:null,Jossa:null,Julbach:null,"Jungingen(Hohenz)":null,Jungnau:null,"Jägerhaus":null,"Jägersfreude":null,"Jänschwalde":null,"Jänschwalde Ost":null,"Jävenitz":null,"Jöhlingen":null,"Jöhlingen":null,"Jöhlingen West":null,"Jößnitz":null,"Jübek":null,"Jüchen":null,"Jülich":null,"Jülich Forschungszentrum":null,"Jülich-Broich":null,"Jülich-Nord":null,"Jülich-Selgersdorf":null,"Jünkerath":null,"Jüterbog":null,"Kaarst IKEA":null,"Kaarst Mitte/Holzbüttgen":null,"Kaarster Bahnhof":null,"Kaarster See":null,Kablow:null,"Kahl Kopp/Heide":null,"Kahl(Main)":null,"Kahl(Main)":null,"Kahla(Thür)":null,Kainzenbad:null,Kaisersesch:null,"Kaiserslautern Hbf":null,"Kaiserslautern Pfaffwerk":null,"Kaiserslautern West":null,Kalchreuth:null,Kaldenkirchen:null,"Kalenborn(Westerw)":null,Kall:null,Kalsow:null,Kalteneck:null,"Kaltenkirchen Süd":null,"Kaltenkirchen(Holst)":null,"Kalthof(Kr Iserlohn)":null,Kamen:null,"Kamen-Methler":null,"Kamenz(Sachs)":null,"Kamp-Bornhofen":null,Kandel:null,Kandern:null,Kanzem:null,"Kapellen-Drusweiler":null,"Kapellen-Wevelinghoven":null,"Kapen Biosphärenreservat":null,Kappelrodeck:null,"Kappelrodeck Ost":null,Kapsweyer:null,Karlsburg:null,Karlsdorf:null,Karlshagen:null,"Karlsruhe Albtalbf":null,"Karlsruhe Bahnhofsvorplatz":null,"Karlsruhe Durlacher Tor":null,"Karlsruhe Entenfang":null,"Karlsruhe Hbf":null,"Karlsruhe Marktplatz (Kaiserstraße)":null,"Karlsruhe Mühlburger Tor":null,"Karlsruhe West":null,"Karlsruhe West":null,"Karlsruhe-Durlach":null,"Karlsruhe-Durlach":null,"Karlsruhe-Hagsfeld":null,"Karlsruhe-Kniel. Rheinbergstr.":null,"Karlsruhe-Knielingen":null,"Karlsruhe-Mühlburg":null,"Karlsruhe-Neureut Kirchfeld":null,"Karlstadt(Main)":null,Karpfham:null,Karsdorf:null,"Karstädt":null,Karthaus:null,Kasbach:null,"Kasbach Brauerei Steffens":null,"Kassel Hbf":null,"Kassel Hbf (tief)":null,"Kassel-Harleshausen":null,"Kassel-Jungfernkopf":null,"Kassel-Kirchditmold":null,"Kassel-Oberzwehren":null,"Kassel-Wilhelmshöhe":null,"Kastl(Oberbay)":null,Katharinenheerd:null,Kating:null,Katlenburg:null,Kattenes:null,Kattenvenne:null,Katzenfurt:null,"Katzhütte":null,Katzwang:null,Katzweiler:null,Kaub:null,Kaufbeuren:null,Kaufering:null,"Kaulsdorf(Saale)":null,"Kavelstorf(Kr Rostock)":null,Kehl:null,Kehlen:null,Keitum:null,Kelkheim:null,"Kelkheim-Hornau":null,"Kelkheim-Münster":null,"Kellmünz":null,Kelsterbach:null,"Kemnath-Neustadt":null,"Kempen(Niederrhein)":null,"Kempten(Allgäu)Hbf":null,"Kempten(Allgäu)Ost":null,Kemtau:null,Kennelgarten:null,Kenz:null,Kenzingen:null,Kerkerbach:null,Kerkwitz:null,Kersbach:null,Kestert:null,Kettwig:null,"Kettwig Stausee":null,Kevelaer:null,Kiebingen:null,"Kiebitzhöhe":null,Kiefersfelden:null,"Kiel Hbf":null,"Kiel Schulen am Langsee":null,"Kiel-Elmschenhagen":null,"Kiel-Hassee CITTI-PARK":null,"Kiel-Russee":null,Killer:null,"Kinding(Altmühltal)":null,Kindsbach:null,"Kirch Göns":null,"Kirch-Jesar":null,"Kirchanschöring":null,"Kirchberg(Murr)":null,"Kirchdorf(Deister)":null,Kirchehrenbach:null,Kirchen:null,Kirchenlaibach:null,"Kirchenlamitz Ost":null,Kirchentellinsfurt:null,"Kirchhain(Bz Kassel)":null,Kirchhammelwarden:null,"Kirchheim(Neckar)":null,"Kirchheim(Teck)":null,"Kirchheim(Teck)-Ötlingen":null,"Kirchheim(Teck)Süd":null,"Kirchheim(Unterfr)":null,"Kirchheim(Weinstr)":null,Kirchheimbolanden:null,Kirchhorsten:null,Kirchhundem:null,Kirchlengern:null,"Kirchmöser":null,Kirchscheidungen:null,Kirchseeon:null,Kirchseeon:null,Kirchweidach:null,Kirchweyhe:null,Kirchzarten:null,Kirkel:null,Kirn:null,"Kirnbach-Grün":null,Kirnsulzbach:null,Kirschbaumwasen:null,Kissing:null,Kitzingen:null,"Kißlegg":null,"Klaffenbach Hp":null,Klais:null,Klandorf:null,"Klanxbüll":null,"Klasdorf Glashütte":null,Klecken:null,"Klein Bünzow":null,"Klein Gerau":null,"Klein Winternheim-Ober Olm":null,Kleinberghofen:null,Kleinenbroich:null,Kleinensiel:null,"Kleinforst Rosensee":null,Kleinfurra:null,"Kleingemünden":null,Kleinheubach:null,Kleinjena:null,Kleinkems:null,"Kleinkötz":null,Kleinostheim:null,"Kleinröhrsdorf":null,Kleinschirma:null,Kleinsteinbach:null,Kleinwallstadt:null,Kleve:null,Klieken:null,Klinge:null,"Klingenberg(Main)":null,"Klingenberg-Colmnitz":null,Klingenbrunn:null,Klingenthal:null,"Klinikum Bremen-Nord/Beckedorf":null,Klitschmar:null,"Kloster Bronnbach":null,"Kloster Marienthal":null,"Kloster Oesede":null,Klosterbuch:null,Klosterfelde:null,Klosterlechfeld:null,Klostermansfeld:null,Klosterreichenbach:null,Klotten:null,"Kläden(Stendal)":null,Knautnaundorf:null,Knesebeck:null,"Knittlingen-Kleinvillars":null,"Knöringen-Essingen":null,"Kobern-Gondorf":null,"Koblenz Hbf":null,"Koblenz Stadtmitte":null,"Koblenz-Ehrenbreitstein":null,"Koblenz-Güls":null,"Koblenz-Lützel":null,"Koblenz-Moselweiß":null,Kochel:null,Kodersdorf:null,Kohlscheid:null,Kohlstetten:null,Kolbermoor:null,Kolkwitz:null,"Kolkwitz Süd":null,Kollmarsreute:null,Kollnau:null,Konstanz:null,"Konstanz-Fürstenberg":null,"Konstanz-Petershausen":null,"Konstanz-Wollmatingen":null,Konz:null,"Konz Mitte":null,"Konz Mitte":null,Korbach:null,"Korbach Süd":null,Kordel:null,Kork:null,Korntal:null,"Korntal Gymnasium":null,"Kornwestheim Pbf":null,Korschenbroich:null,Koserow:null,"Kothmaißling":null,Kottenheim:null,Kraftsdorf:null,"Kraftwerk Finkenheerd":null,Kraghammer:null,Kranichfeld:null,Kratzeburg:null,Krauthausen:null,Kredenbach:null,"Krefeld Hbf":null,"Krefeld-Hohenbudberg Chempark":null,"Krefeld-Linn":null,"Krefeld-Linn":null,"Krefeld-Oppum":null,"Krefeld-Uerdingen":null,Kreiensen:null,"Kreimbach-Kaulbach":null,Kremmen:null,Krempe:null,Kremperheide:null,Krensitz:null,Kressbronn:null,"Kretscham-Rothensehma":null,"Kreuz Konz":null,"Kreuzau Bahnhof":null,"Kreuzau-Eifelstraße":null,"Kreuzberg(Ahr)":null,"Kreuzstraße":null,Kreuztal:null,"Kreuztal-Littfeld":null,Kriftel:null,Krippen:null,Kronach:null,"Kronberg Süd":null,"Kronberg(Taunus)":null,Kronshagen:null,Kronskamp:null,Kronweiler:null,Kruft:null,"Krumbach(Schw)Schule":null,"Krumbach(Schwab)":null,Krumhermsdorf:null,Krumpa:null,Krupunder:null,"Krölpa-Ranis":null,"Kröpelin":null,"Kubschütz":null,Kuchen:null,Kulmbach:null,"Kummerow(Stralsund)":null,"Kummersdorf(Storkow)":null,Kunersdorf:null,"Kupfermühle":null,Kuppenheim:null,"Kurort Altenberg(Erzgebirge)":null,"Kurort Jonsdorf":null,"Kurort Jonsdorf Hst":null,"Kurort Oberwiesenthal":null,"Kurort Oybin":null,"Kurort Oybin-Niederdorf":null,"Kurort Rathen":null,Kusel:null,Kutenholz:null,Kutzenhausen:null,Kyhna:null,Kyllburg:null,Kyritz:null,"Kälberau":null,"Kämmereiforst":null,"Köditz":null,"Köfering":null,"Kölleda":null,"Köln Airport-Businesspark":null,"Köln Frankfurter Straße":null,"Köln Geldernstr./Parkgürtel":null,"Köln Hansaring":null,"Köln Hbf":null,"Köln Messe/Deutz":null,"Köln Messe/Deutz Gl. 9-10":null,"Köln Messe/Deutz Gl.11-12":null,"Köln Steinstraße":null,"Köln Süd":null,"Köln Trimbornstr":null,"Köln Volkhovener Weg":null,"Köln West":null,"Köln-Blumenberg":null,"Köln-Buchforst":null,"Köln-Chorweiler":null,"Köln-Chorweiler Nord":null,"Köln-Dellbrück":null,"Köln-Ehrenfeld":null,"Köln-Ehrenfeld":null,"Köln-Holweide":null,"Köln-Longerich":null,"Köln-Mülheim":null,"Köln-Müngersdorf Technologiepark":null,"Köln-Nippes":null,"Köln-Nippes":null,"Köln-Stammheim":null,"Köln-Weiden West":null,"Köln-Worringen":null,"Köln/Bonn Flughafen":null,"Kölpinsee":null,"Köndringen":null,"Königs Wusterhausen":null,"Königsbach(Baden)":null,"Königsborn":null,"Königsbronn":null,"Königsbrück":null,"Königschaffhausen":null,"Königshofen(Baden)":null,"Königshofen(Kahl)":null,"Königslutter":null,"Königsstollen":null,"Königstein(Sächs Schw)":null,"Königstein(Taunus)":null,"Königswinter":null,"Königswinter Fähre":null,"Königswinter, Clem.-August-Str.":null,"Könitz(Thür)":null,"Könnern":null,"Köppern":null,"Körle":null,"Köthen":null,"Köttewitz":null,"Kötzschau":null,"Kövenig":null,"Kühnhausen":null,"Kühren":null,"Külte-Wetterburg":null,"Künsebeck":null,"Küntrop":null,"Küps":null,"Kürbitz":null,"Küstrin-Kietz":null,Laaber:null,"Laage(Meckl)":null,Laberweinting:null,Ladenburg:null,"Lage(Lippe)":null,Lagerlechfeld:null,"Lahr(Schwarzw)":null,Laineck:null,Lalendorf:null,Lam:null,"Lambrecht(Pfalz)":null,Lambsheim:null,Lampertheim:null,"Lampertsmühle-Otterbach":null,Lampertswalde:null,Lancken:null,"Landau(Isar)":null,"Landau(Pfalz)Hbf":null,"Landau(Pfalz)Süd":null,"Landau(Pfalz)West":null,"Landsberg(L)Schule":null,"Landsberg(Lech)":null,"Landsberg(b. Halle/Saale)":null,"Landsberg(b. Halle/Saale) Süd":null,"Landshut(Bay)Hbf":null,"Landshut(Bay)Süd":null,Landstuhl:null,"Landsweiler-Reden":null,"Lang Göns":null,Langdorf:null,"Langebrück(Sachs)":null,"Langeln(Holst)":null,Langelsheim:null,"Langen(Hess)":null,"Langen-Flugsicherung":null,Langenargen:null,"Langenau(Württ)":null,"Langenbach(Oberbay)":null,Langenbrand:null,Langendorf:null,"Langeneichstädt":null,"Langenfeld(Rhld)":null,"Langenfeld(Rhld)-Berghausen":null,"Langenhagen Mitte":null,"Langenhagen Pferdemarkt":null,"Langenhagen-Kaltenweide":null,Langenhahn:null,"Langenhorn(Schlesw)":null,Langenlonsheim:null,Langenmoor:null,"Langenorla Ost":null,"Langenorla West":null,Langenprozelten:null,Langenselbold:null,Langenstein:null,"Langensteinbach Bahnhof":null,"Langenwang(Schwab)":null,Langenweddingen:null,Langenwolmsdorf:null,"Langenwolmsdorf Mitte":null,Langenzenn:null,Langerwehe:null,Langhagen:null,Langlau:null,"Langquaid(b Eggmühl)":null,"Langsdorf(Oberhess)":null,Langwedel:null,"Langweid(Lech)":null,Lathen:null,Laubendorf:null,"Laubenheim(Nahe)":null,"Laucha(Unstrut)":null,Lauchhammer:null,Lauchheim:null,Lauchringen:null,"Lauchringen West":null,Lauda:null,"Laudenbach am Main":null,"Laudenbach(Bergstr)":null,"Laudenbach(Württ)":null,"Lauenbrück":null,"Lauenburg(Elbe)":null,"Lauenförde-Beverungen":null,"Lauenstein(Sachs)":null,"Lauf West":null,"Lauf(links Pegnitz)":null,"Lauf(rechts Pegnitz)":null,Laufach:null,"Laufen(Oberbay)":null,"Laufenburg(Baden)":null,"Laufenburg(Baden)Ost":null,"Lauffen(Neckar)":null,Lauingen:null,"Laupheim Stadt":null,"Laupheim West":null,"Laurenburg(Lahn)":null,"Lauscha(Thür)":null,"Lauta(Nl)":null,"Lautenbach(Baden)":null,"Lauter(Sachs)":null,"Lauterbach Mole":null,"Lauterbach(Hess)Nord":null,"Lauterbach(Rügen)":null,"Lauterbach-Steinbach":null,"Lauterecken-Grumbach":null,"Laußig(Düben)":null,"Laußnitz":null,Lebach:null,"Lebach-Jabach":null,"Leer(Ostfriesl)":null,"Leese-Stolzenau":null,Legden:null,Legefeld:null,Legelshurst:null,Lehmen:null,"Lehndorf(Altenburg)":null,Lehnheim:null,Lehnitz:null,Lehrte:null,Leichlingen:null,"Leiferde(b Gifhorn)":null,Leimstruth:null,Leinefelde:null,Leinfelden:null,Leingarten:null,"Leingarten Mitte":null,"Leingarten Ost":null,"Leingarten West":null,Leipheim:null,"Leipzig Allee-Center":null,"Leipzig Anger-Crottendorf":null,"Leipzig Bayerischer Bahnhof":null,"Leipzig Coppiplatz":null,"Leipzig Grünauer Allee":null,"Leipzig Hbf":null,"Leipzig Hbf (tief)":null,"Leipzig Karlsruher Str":null,"Leipzig MDR":null,"Leipzig Markt":null,"Leipzig Messe":null,"Leipzig Messe":null,"Leipzig Miltitzer Allee":null,"Leipzig Nord":null,"Leipzig Olbrichtstraße":null,"Leipzig Slevogtstraße":null,"Leipzig Völkerschlachtdenkmal":null,"Leipzig Werkstättenstraße":null,"Leipzig Wilhelm-Leuschner-Platz":null,"Leipzig-Connewitz":null,"Leipzig-Engelsdorf":null,"Leipzig-Engelsdorf":null,"Leipzig-Gohlis":null,"Leipzig-Heiterblick":null,"Leipzig-Holzhausen":null,"Leipzig-Knauthain":null,"Leipzig-Leutzsch":null,"Leipzig-Liebertwolkwitz":null,"Leipzig-Lindenau":null,"Leipzig-Lützschena":null,"Leipzig-Miltitz":null,"Leipzig-Möckern":null,"Leipzig-Mölkau":null,"Leipzig-Paunsdorf":null,"Leipzig-Paunsdorf":null,"Leipzig-Plagwitz":null,"Leipzig-Rückmarsdorf":null,"Leipzig-Sellerhausen":null,"Leipzig-Stötteritz":null,"Leipzig-Thekla":null,"Leipzig-Wahren":null,"Leipzig/Halle Flughafen":null,Leisnig:null,Leitstade:null,"Leißling":null,Lembeck:null,"Lemförde":null,Lemgo:null,"Lemgo-Lüttfeld":null,Lemmie:null,Lendringsen:null,"Lengede-Broistedt":null,"Lengefeld-Rauenstein":null,"Lengenfeld(Vogtl)":null,Lengenwang:null,"Lengerich(Westf)":null,Lenggries:null,Lenglern:null,"Lennestadt-Altenhundem":null,"Lennestadt-Grevenbrück":null,"Lennestadt-Meggen":null,Lensahn:null,"Lentföhrden":null,Leonberg:null,"Leopoldshafen Leopoldstr.":null,Leopoldstal:null,Leschede:null,Letmathe:null,"Letmathe Dechenhöhle":null,Letschin:null,"Lette(Kr Coesfeld)":null,Letter:null,Letter:null,Leubingen:null,"Leubsdorf(Rhein)":null,"Leubsdorf(Sachs)":null,"Leun/Braunfels":null,"Leuna Werke Nord":null,"Leuna Werke Süd":null,Leutenberg:null,Leuterschach:null,"Leutesdorf(Rhein)":null,"Leuthen(Cottbus)":null,Leutkirch:null,"Leverkusen Chempark":null,"Leverkusen Mitte":null,"Leverkusen Mitte":null,"Leverkusen-Küppersteg":null,"Leverkusen-Küppersteg":null,"Leverkusen-Rheindorf":null,"Leverkusen-Schlebusch":null,"Lich(Oberhess)":null,"Lichtenberg(Erzgeb)":null,Lichtenfels:null,"Lichtenhain(a d Bergbahn)":null,"Lichtenstein Ernst-Schneller-Siedlung":null,"Lichtenstein Gewerbegebiet":null,"Lichtenstein Hartensteiner Straße":null,"Lichtenstein(Sachs)":null,"Lichtentanne(Sachs)":null,"Lichtentanne(Thür)":null,Lichtenthal:null,"Liebenthal(Prignitz)":null,Lieblos:null,Liederbach:null,"Liederbach-Süd":null,"Lietzow(Rügen)":null,"Limbach(Vogtl)":null,"Limbach(b Homburg,Saar)":null,"Limburg Süd":null,"Limburg(Lahn)":null,Limburgerhof:null,"Limmritz(Sachs)":null,"Linda(Elster)":null,Lindach:null,"Lindau Hbf":null,"Lindau-Aeschach":null,"Lindenberg(Mark)":null,Lindenholzhausen:null,Lindern:null,"Lindhorst(Schaumb-Lippe)":null,"Lindow(Mark)":null,Lindwedel:null,"Lingen(Ems)":null,Lingenfeld:null,"Linkenheim Rathaus":null,"Linnich Bhf":null,"Linnich-Tetz":null,Linsburg:null,Linsenhofen:null,"Linz(Rhein)":null,Lippstadt:null,Lispenhausen:null,Lissendorf:null,Listerscheid:null,"Lobstädt":null,Lochham:null,"Lohgarten-Roth":null,Lohhof:null,Lohmen:null,"Lohne(Oldb)":null,Lohnweiler:null,"Lohr Bahnhof":null,"Loitsch-Hohenleuben":null,Lollar:null,Lonsee:null,Loppenhausen:null,"Lorch(Rhein)":null,"Lorch(Württ)":null,Lorchhausen:null,Lorsbach:null,Lorsch:null,Lottschesee:null,Loxstedt:null,"Loßburg-Rodt":null,Lubolz:null,Luckaitztal:null,"Luckau-Uckro":null,Luckenau:null,Luckenwalde:null,Ludersheim:null,"Ludwigsau-Friedlos":null,Ludwigsburg:null,Ludwigschorgast:null,Ludwigsfelde:null,"Ludwigsfelde-Struveshof":null,"Ludwigshafen(Bodensee)":null,"Ludwigshafen(Rh)Hbf":null,"Ludwigshafen(Rh)Hbf":null,"Ludwigshafen(Rhein) BASF Mitte":null,"Ludwigshafen(Rhein) BASF Nord":null,"Ludwigshafen(Rhein) BASF Süd":null,"Ludwigshafen(Rhein) Mitte":null,"Ludwigshafen-Mundenheim":null,"Ludwigshafen-Oggersheim":null,"Ludwigshafen-Rheingönheim":null,"Ludwigshöhe":null,Ludwigslust:null,Ludwigsstadt:null,Ludwigsthal:null,Luhe:null,"Luhe-Wildenau":null,"Luisenthal(Saar)":null,Lunden:null,Lunestedt:null,"Lutherstadt Eisleben":null,"Lutherstadt Wittenberg Altstadt":null,"Lutherstadt Wittenberg Hbf":null,"Lutherstadt Wittenberg-Labetz":null,"Lutherstadt Wittenberg-Piesteritz":null,Lutten:null,Lutum:null,"Löbau(Sachs)":null,"Löcherberg":null,"Löcknitz":null,"Lödingsen":null,"Löf":null,"Löffingen":null,"Löhnberg":null,"Löhne(Westf)":null,"Lörrach Dammstraße":null,"Lörrach Hbf":null,"Lörrach Museum/Burghof":null,"Lörrach Schwarzwaldstraße":null,"Lörrach-Brombach/Hauingen":null,"Lörrach-Haagen/Messe":null,"Lörrach-Stetten":null,"Lörzenbach-Fahrenbach":null,"Lövenich":null,"Lövenich":null,"Löwenberg(Mark)":null,"Löwental":null,"Lößnitz ob Bf":null,"Lößnitz unt Bf":null,"Lößnitzgrund":null,"Lübbecke(Westf)":null,"Lübben(Spreewald)":null,"Lübbenau(Spreewald)":null,"Lübberstedt":null,"Lübeck Flughafen":null,"Lübeck Hbf":null,"Lübeck Hochschulstadtteil":null,"Lübeck St Jürgen":null,"Lübeck-Dänischburg IKEA":null,"Lübeck-Kücknitz":null,"Lübeck-Travem. Skandinavienkai":null,"Lübeck-Travemünde Hafen":null,"Lübeck-Travemünde Strand":null,"Lüblow(Meckl)":null,"Lübs(Magdeburg)":null,"Lübstorf":null,"Lüchtringen":null,"Lüdenscheid":null,"Lüdersdorf(Meckl)":null,"Lüdinghausen":null,"Lügde":null,"Lüneburg":null,"Lüneburg":null,"Lünen Hbf":null,"Lünern":null,"Lüssow(Meckl)":null,"Lütter":null,"Lützel":null,"Lützow":null,"Maasbüll(b Niebüll)":null,"Machern(Sachs)":null,"Magdeburg Hasselbachplatz":null,"Magdeburg Hbf":null,"Magdeburg Herrenkrug":null,"Magdeburg SKET Industriepark":null,"Magdeburg Südost":null,"Magdeburg-Buckau":null,"Magdeburg-Eichenweiler":null,"Magdeburg-Neustadt":null,"Magdeburg-Rothensee":null,"Magdeburg-Rothensee":null,"Magdeburg-Salbke":null,"Magdeburg-Sudenburg":null,Magstadt:null,Mahlow:null,Mahlwinkel:null,Maichingen:null,"Maichingen Nord":null,"Maikammer-Kirrweiler":null,Mainaschaff:null,"Mainhausen Zellhausen":null,Mainleus:null,Mainroth:null,"Maintal Ost":null,"Maintal West":null,"Mainz Hbf":null,"Mainz Nord":null,"Mainz Römisches Theater":null,"Mainz-Bischofsheim":null,"Mainz-Bischofsheim":null,"Mainz-Gonsenheim":null,"Mainz-Gustavsburg":null,"Mainz-Kastel":null,"Mainz-Laubenheim":null,"Mainz-Marienborn":null,"Mainz-Mombach":null,Maisach:null,Malchin:null,"Malching(Oberbay)":null,Mallersdorf:null,Malmsheim:null,Malsch:null,"Malsch Süd":null,Malsfeld:null,"Malsfeld-Beiseförth":null,Malter:null,Mammendorf:null,Mandern:null,Manebach:null,Manndorf:null,"Mannheim ARENA/Maimarkt":null,"Mannheim ARENA/Maimarkt":null,"Mannheim Handelshafen":null,"Mannheim Hbf":null,"Mannheim-Friedrichsfeld Süd":null,"Mannheim-Käfertal":null,"Mannheim-Luzenberg":null,"Mannheim-Neckarau":null,"Mannheim-Neckarstadt":null,"Mannheim-Rheinau":null,"Mannheim-Seckenheim":null,"Mannheim-Waldhof":null,"Marbach Ost (Villingen-Schwenningen)":null,"Marbach West(Villingen-Schwenningen)":null,"Marbach(Neckar)":null,"Marbach(b Münsingen)":null,"Marbach-Grafeneck":null,"Marbeck-Heiden":null,"Marburg Süd":null,"Marburg(Lahn)":null,"Maria Rain":null,"Maria Veen":null,Marienborn:null,Marienhafe:null,Marienheide:null,"Markdorf(Baden)":null,Marke:null,Markelfingen:null,Markelsheim:null,Markkleeberg:null,"Markkleeberg Nord":null,"Markkleeberg-Gaschwitz":null,"Markkleeberg-Großstädteln":null,"Markranstädt":null,Marksuhl:null,"Markt Bibart":null,"Markt Erlbach":null,"Markt Indersdorf":null,"Markt Schwaben":null,Marktbreit:null,Marktl:null,Marktleuthen:null,Marktoberdorf:null,"Marktoberdorf Schule":null,Marktredwitz:null,Marktschorgast:null,"Marl Mitte":null,"Marl-Hamm":null,"Marl-Sinsen":null,Marlishausen:null,Marnheim:null,Marquardt:null,Marsberg:null,"Marstetten-Aitrach":null,Martensdorf:null,Martinlamitz:null,Martinroda:null,Martinstein:null,Martinstein:null,"Martinszell(Allgäu)":null,"Marxgrün":null,Marxzell:null,Marzling:null,Maschen:null,Maselheim:null,Massen:null,Massing:null,Matzing:null,Maubach:null,"Mauer(b Heidelberg)":null,"Maulbronn Stadt/Kloster":null,"Maulbronn West":null,"Maulbronn West":null,Maulburg:null,Mausheim:null,Mauthaus:null,Maxau:null,"Maxhütte-Haidhof":null,"Maximiliansau Eisenbahnstraße":null,"Maximiliansau West":null,"Maximiliansau-Im Rüsten":null,"Mayen Ost":null,"Mayen West":null,"Mayschoß":null,Mechernich:null,"Mechterstädt":null,Meckelfeld:null,Meckenbeuren:null,"Meckenheim Industriepark":null,"Meckenheim Kottenforst":null,"Meckenheim(Bz Köln)":null,Meckesheim:null,"Medewitz(Mark)":null,Meeder:null,Meerane:null,"Meerbusch-Osterath":null,Meeschensee:null,Mehltheuer:null,Mehrhoog:null,Meine:null,"Meinersdorf(Erzgeb)":null,Meinersen:null,Meinerzhagen:null,Meiningen:null,Meinsdorf:null,Meitingen:null,Meitzendorf:null,"Meißen":null,"Meißen Altstadt":null,"Meißen Triebischtal":null,Melbach:null,Melchow:null,Meldorf:null,Melle:null,"Mellenbach-Glasbach":null,Mellendorf:null,"Mellingen(Thür)":null,"Mellrichstadt Bf":null,Melsdorf:null,Melsungen:null,"Melsungen Bartenwetzerbrücke":null,"Melsungen-Röhrenfurth":null,"Melsungen-Röhrenfurth":null,Memmingen:null,"Menden(Rheinl)":null,"Menden(Sauerland)":null,"Menden(Sauerland)Süd":null,Mendig:null,Mengen:null,Mengeringhausen:null,"Mengersgereuth-Hämmern":null,"Mengersgereuth-Hämmern Ost":null,"Menzingen(Baden)":null,Meppen:null,Merching:null,Merchweiler:null,Mering:null,"Mering-St Afra":null,"Mersch(Westf)":null,Merseburg:null,"Merseburg Bergmannsring":null,"Merten(Sieg)":null,Mertesheim:null,"Mertingen Bahnhof":null,Merzenich:null,"Merzig(Saar)":null,"Merzig(Saar) Stadtmitte":null,"Mesch Neue Mühle":null,Meschede:null,Messel:null,Messinghausen:null,"Metelen Land":null,Mettenheim:null,Mettlach:null,"Mettmann Stadtwald":null,"Mettmann Zentrum":null,"Metzingen(Württ)":null,"Metzingen-Neuhausen":null,"Meuselbach-Schwarzmühle":null,Meyenburg:null,"Meßdorf":null,"Michelau(Oberfr)":null,"Michelau(Württ)":null,"Michelaubrück":null,"Michelbach(Unterfr)":null,"Michelbach(Unterfr) Herrnmühle":null,Michelstadt:null,Michendorf:null,"Miedelsbach-Steinenberg":null,Miesbach:null,Miesenbach:null,Miesenheim:null,Mieste:null,Miesterhorst:null,"Millingen(b Rees)":null,"Millingen(b Rheinb)":null,Miltach:null,Miltenberg:null,Miltern:null,Miltzow:null,Mimberg:null,Mindelaltheim:null,Mindelheim:null,"Minden(Westf)":null,Mirow:null,Mistorf:null,"Mittel Gründau":null,Mittelherwigsdorf:null,Mitteloelsnitz:null,Mittelschmalkalden:null,Mittelsinn:null,Mittenwald:null,Mittergars:null,Mittweida:null,Mixdorf:null,Mochenwangen:null,Mockrehna:null,Moers:null,Moidentin:null,"Monbach-Neuhausen":null,Monreal:null,Monsheim:null,Montabaur:null,Montabaur:null,Monzingen:null,Moorbekhalle:null,Moosbachtal:null,Moosburg:null,Moosrain:null,Moritzburg:null,Morlesau:null,Morsum:null,"Mosbach West":null,"Mosbach(Baden)":null,"Mosbach-Neckarelz":null,Mosel:null,Moselkern:null,Mudersbach:null,Muggensturm:null,"Muggensturm Badesee":null,"Muhr a See":null,"Mulda(Sachs)":null,"Muldenberg Floßplatz":null,"Muldenhütten":null,Muldenstein:null,"Mulsum-Essel":null,Munderkingen:null,"Munster(Örtze)":null,"Murg(Baden)":null,Murnau:null,"Murnau Ort":null,Murrhardt:null,"Mußbach":null,"Mägdesprung":null,"Mägerkingen":null,"Möckmühl":null,"Mögelin":null,"Mögglingen(Gmünd)":null,"Möhringen Bahnhof":null,"Möhringen Rathaus":null,"Mölln(Lauenb)":null,"Mölln(Meckl)":null,"Mömbris-Mensengesäß":null,"Mömbris-Strötzbach":null,"Mönchengladbach Hbf":null,"Mönchengladbach-Genhausen":null,"Mönchengladbach-Lürrip":null,"Mönchengladbach-Rheindahlen":null,"Mönchhagen":null,"Mönchröden":null,"Mörfelden":null,"Möringen(Altm)":null,"Mörlenbach":null,"Möser":null,"Mössingen":null,"Möttingen":null,"Mücheln(Geiseltal)":null,"Mücheln(Geiseltal) Stadt":null,"Mücke(Hess)":null,"Müden(Mosel)":null,"Mügeln Bf":null,"Mügeln Stadt":null,"Mühlacker":null,"Mühlacker Rößlesweg":null,"Mühlanger":null,"Mühlbach(Pirna)":null,"Mühldorf(Oberbay)":null,"Mühlen(Oldb)":null,"Mühlen(b Horb)":null,"Mühlenbeck-Mönchmühle":null,"Mühlhausen(Thür)":null,"Mühlhausen(b Engen)":null,"Mühlheim(Main)":null,"Mühlheim(Main)-Dietesheim":null,"Mühlheim(b Tuttlingen)":null,"Mühlstetten":null,"Mühltal":null,"Mühringen":null,"Mülheim(Ruhr)Hbf":null,"Mülheim(Ruhr)Styrum":null,"Mülheim(Ruhr)West":null,"Müllheim(Baden)":null,"Müllrose":null,"Münchberg":null,"Müncheberg(Mark)":null,"Münchehof(Harz)":null,"München Donnersbergerbrücke":null,"München Flughafen Besucherpark":null,"München Flughafen Terminal":null,"München Flughafen Terminal":null,"München Hackerbrücke":null,"München Harras":null,"München Hbf":null,"München Hbf (tief)":null,"München Hbf Gl.27-36":null,"München Hbf Gl.5-10":null,"München Heimeranplatz":null,"München Heimeranplatz":null,"München Hirschgarten":null,"München Isartor":null,"München Karlsplatz":null,"München Leienfelsstr.":null,"München Leuchtenbergring":null,"München Marienplatz":null,"München Ost":null,"München Rosenheimer Platz":null,"München Siemenswerke":null,"München St.Martin-Str.":null,"München(Bad Berka)":null,"München-Allach":null,"München-Aubing":null,"München-Berg am Laim":null,"München-Daglfing":null,"München-Englschalking":null,"München-Fasanerie":null,"München-Fasangarten":null,"München-Feldmoching":null,"München-Freiham":null,"München-Giesing":null,"München-Johanneskirchen":null,"München-Karlsfeld":null,"München-Laim":null,"München-Langwied":null,"München-Lochhausen":null,"München-Mittersendling":null,"München-Moosach":null,"München-Neuaubing":null,"München-Neuperlach Süd":null,"München-Obermenzing":null,"München-Pasing":null,"München-Pasing":null,"München-Perlach":null,"München-Riem":null,"München-Solln":null,"München-Trudering":null,"München-Untermenzing":null,"München-Westkreuz":null,"Münchhausen":null,"Münchingen":null,"Münchingen Rührberg":null,"Münchsmünster":null,"Münchweiler(Alsenz)":null,"Münchweiler(Rodalb)":null,"Münnerstadt":null,"Münsingen":null,"Münster(W)Zentrum Nord":null,"Münster(Westf)Hbf":null,"Münster(Westf)Hbf":null,"Münster(b Dieburg)":null,"Münster-Albachten":null,"Münster-Amelsbüren":null,"Münster-Hiltrup":null,"Münster-Häger":null,"Münster-Roxel":null,"Münster-Sarmsheim":null,"Münster-Sprakel":null,"Münstertal(Schwarzwald)":null,"Münzesheim":null,"Münzesheim Ost":null,"Mürlenbach":null,"Müssen":null,Nabburg:null,"Nachterstedt-Hoym":null,Nackenheim:null,Nagold:null,"Nagold Stadtmitte":null,"Nagold-Iselshausen":null,"Nagold-Steinberg":null,Naila:null,Namborn:null,Namedy:null,Namedy:null,Narsdorf:null,"Nassau(Erzgeb)":null,"Nassau(Lahn)":null,Nassenbeuren:null,Nassenheide:null,"Natrup-Hagen":null,Nauen:null,"Nauendorf(Saalkr)":null,"Nauheim(b Gr.Gerau)":null,"Naumburg(Saale)Hbf":null,"Naumburg(Saale)Ost":null,"Naumburg-Roßbach":null,"Naundorf (b Oschatz)":null,Naunhof:null,Neanderthal:null,Nebitzschen:null,Nebra:null,Nechlin:null,"Neckarbischofsheim Helmhof":null,"Neckarbischofsheim Nord":null,"Neckarbischofsheim Stadt":null,Neckarburken:null,"Neckargemünd":null,"Neckargemünd Altstadt":null,Neckargerach:null,"Neckarhausen bei Neckarsteinach":null,Neckarsteinach:null,Neckarsulm:null,Neckarsulm:null,"Neckarsulm Mitte":null,"Neckarsulm Nord":null,"Neckarsulm Süd":null,Neckarzimmern:null,Neef:null,Neetzendorf:null,Neetzka:null,"Neheim-Hüsten":null,Nehren:null,Neidenfels:null,Neidenstein:null,Neinstedt:null,Nellmersbach:null,Nemmenich:null,"Nemsdorf-Göhrendorf":null,"Nendingen(b Tuttlingen)":null,Nennhausen:null,Nennig:null,"Nennigmühle":null,Nenzingen:null,Nersingen:null,Nesselwang:null,Nettersheim:null,Netzeband:null,Netzkater:null,Netzschkau:null,"Neu Isenburg":null,"Neu Isenburg":null,"Neu Pudagla":null,"Neu St Jürgen":null,"Neu Wokern":null,"Neu Wulmstorf":null,"Neu-Anspach":null,"Neu-Edingen/Friedrichsfeld":null,"Neu-Ulm":null,Neubeckum:null,Neubeckum:null,Neubiberg:null,Neubrandenburg:null,"Neubrücke(Nahe)":null,Neubukow:null,"Neuburg(Donau)":null,"Neuburg(Kammel)":null,"Neuburg(Rhein)":null,"Neubäu":null,Neudenau:null,Neudietendorf:null,"Neudorf(Erzgeb)":null,"Neudorf-Platendorf":null,"Neue Schenke":null,"Neuenburg(Baden)":null,"Neuenbürg(Enz)":null,"Neuenbürg(Enz) Eyachbrücke":null,"Neuenbürg(Enz) Freibad":null,"Neuenbürg(Enz) Süd":null,Neuendettelsau:null,"Neuenhagen(b Berlin)":null,"Neuenkirchen(Oldb)":null,"Neuenmarkt-Wirsberg":null,Neuenrade:null,Neuenstein:null,"Neufahrn(Niederbay)":null,"Neufahrn(b Freising)":null,Neuffen:null,"Neufra(Hohenz)":null,Neugersdorf:null,Neugilching:null,"Neuhaus am Rennweg":null,"Neuhaus(Pegnitz)":null,"Neuhaus-Igelshieb":null,"Neuhausen Bad Bf":null,"Neuhausen(Cottbus)":null,"Neuhausen(b Landshut)":null,"Neuhof(Kr Fulda)":null,"Neuhof(b Zossen)":null,Neukieritzsch:null,"Neukirch(Lausitz)Ost":null,"Neukirch(Lausitz)West":null,"Neukirchen(Inn)":null,"Neukirchen(b Sulzb)":null,"Neukirchen-Klaffenbach":null,"Neukirchen-Wyhra":null,"Neukloster(Kr Stade)":null,"Neulußheim":null,"Neumark(Sachs)":null,"Neumarkt(Oberpf)":null,"Neumarkt-St Veit":null,"Neumühle(Elster)":null,"Neumünster":null,"Neumünster Stadtwald":null,"Neumünster Süd AKN":null,"Neundorf(Anh)":null,Neunhofen:null,Neunkirch:null,"Neunkirchen a Sand":null,"Neunkirchen(Kr Siegen)":null,"Neunkirchen(Saar)-Wellesweiler":null,"Neunkirchen(Saar)Hbf":null,Neuoelsnitz:null,Neupetershain:null,"Neuruppin Rheinsberger Tor":null,"Neuruppin West":null,"Neuruppin West":null,"Neusalza-Spremberg":null,"Neuses(b Kronach)":null,Neusorg:null,"Neuss Allerheiligen":null,"Neuss Am Kaiser":null,"Neuss Hbf":null,"Neuss Hbf":null,"Neuss Hbf":null,"Neuss Rheinparkcenter":null,"Neuss Süd":null,"Neustadt am Rübenberge":null,"Neustadt(Aisch)Bahnhof":null,"Neustadt(Aisch)Mitte":null,"Neustadt(Donau)":null,"Neustadt(Dosse)":null,"Neustadt(Holst)":null,"Neustadt(Holst)Gbf":null,"Neustadt(Kr Marburg)":null,"Neustadt(Orla)":null,"Neustadt(Sachs)":null,"Neustadt(Schwarzw)":null,"Neustadt(Waldnaab)":null,"Neustadt(Weinstr) Süd":null,"Neustadt(Weinstr)Hbf":null,"Neustadt(b Coburg)":null,"Neustadt-Böbig":null,"Neustadt-Glewe":null,"Neustadt-Hohenacker":null,"Neustift(b Passau)":null,"Neustrelitz Hbf":null,"Neusäß":null,"Neusörnewitz":null,Neutrebbin:null,Neuwied:null,"Neuwirtshaus(Porscheplatz)":null,Neuzelle:null,"Neuötting":null,Nidda:null,Nidderau:null,Nidderau:null,"Nidderau-Eichen":null,"Nidderau-Windecken":null,"Nideggen-Brück":null,"Niebüll":null,"Niebüll neg":null,"Niebüll, Sylt Shuttle":null,Niedaltdorf:null,"Nieder Flörsheim-Dalsheim":null,"Nieder Ohmen":null,"Nieder Olm":null,"Nieder Wöllstadt":null,Niederau:null,"Niederau-Tuchmühle":null,Niederbiegen:null,Niederbobritzsch:null,Niederbrechen:null,Niederdollendorf:null,"Niederdorf(Erzgeb)":null,Niederdorfelden:null,Niederdreisbach:null,Niederdresselndorf:null,Niedererbach:null,Niederfinow:null,"Niedergörsdorf":null,Niederhadamar:null,Niederheimbach:null,"Niederhöchstadt":null,"Niederhövels":null,Niederjosbach:null,Niederlahnstein:null,Niederlehme:null,Niederlindhart:null,Niederlinxweiler:null,Niedermittlau:null,Niedermohr:null,Niederndodeleben:null,"Niedernhausen(Taunus)":null,Niederoderwitz:null,"Niederpöllnitz":null,Niederraunau:null,Niederroth:null,Niedersachswerfen:null,"Niedersachswerfen Herkulesmarkt":null,"Niedersachswerfen Ilfelder Straße":null,"Niedersachswerfen Ost":null,"Niederscheld(Dillkr)Süd":null,Niederschelden:null,"Niederschelden Nord":null,Niederschlag:null,Niederschlottwitz:null,Niederschmalkalden:null,Niederselters:null,Niederspier:null,Niedersteinbach:null,Niederstetten:null,Niederstotzingen:null,Niedertrebra:null,Niederwalgern:null,Niederwalluf:null,Niederwartha:null,Niederweimar:null,Niederwiesa:null,Niederwillingen:null,Niederwinden:null,"Niederwürschnitz":null,Niederzeuzheim:null,Niederzissen:null,"Niederzwönitz":null,Niefern:null,Niemberg:null,"Nienburg(Saale)":null,"Nienburg(Weser)":null,"Nienhagen(Halberst)":null,Nierstein:null,Nieukerk:null,Nievenheim:null,Nievern:null,Niklashausen:null,"Nimburg(Baden)":null,"Nistertal-Bad Marienberg":null,Nittel:null,Nohen:null,Nohfelden:null,"Nohra(Weimar)":null,"Nohra(Wipper)":null,Nonnenhorn:null,"Nordbögge":null,Norddeich:null,"Norddeich Mole":null,Norden:null,Nordendorf:null,Nordenham:null,"Norderstedt Mitte":null,"Nordhalben Bf":null,Nordhastedt:null,Nordhausen:null,"Nordhausen Bahnhofsplatz":null,"Nordhausen Hesseröder Straße":null,"Nordhausen Nord":null,"Nordhausen Ricarda-Huch-Straße":null,"Nordhausen Schurzfell":null,"Nordhausen-Altentor":null,"Nordhausen-Krimderode":null,"Nordhausen-Salza":null,"Nordheim(Württ)":null,
-Nordholz:null,Nordsode:null,Nordstemmen:null,Nordwalde:null,Norf:null,Norheim:null,Norsingen:null,"Northeim(Han)":null,Nortorf:null,Nossentin:null,"Nottuln-Appelhülsen":null,Nufringen:null,"Nußberg-Schönau":null,"Nöbdenitz":null,"Nördlingen":null,"Nörten-Hardenberg":null,"Nünchritz":null,"Nürnberg Frankenstadion":null,"Nürnberg Frankenstadion Sonderbahnsteig":null,"Nürnberg Hbf":null,"Nürnberg Nordost":null,"Nürnberg Ost":null,"Nürnberg Ostring":null,"Nürnberg Rothenburger Str.":null,"Nürnberg-Dutzendteich":null,"Nürnberg-Dürrenhof":null,"Nürnberg-Eibach":null,"Nürnberg-Erlenstegen":null,"Nürnberg-Gleißhammer":null,"Nürnberg-Laufamholz":null,"Nürnberg-Mögeldorf":null,"Nürnberg-Rehhof":null,"Nürnberg-Reichelsdorf":null,"Nürnberg-Reichelsdorf":null,"Nürnberg-Sandreuth":null,"Nürnberg-Schweinau":null,"Nürnberg-Stein":null,"Nürnberg-Steinbühl":null,"Nürnberg-Steinbühl":null,"Nürtingen":null,"Nürtingen-Roßdorf":null,"Nürtingen-Vorstadt":null,"Nützen":null,"Ober Ramstadt":null,"Ober Widdersheim":null,Oberachern:null,"Oberachern Bindfadenfabrik":null,Oberaichen:null,Oberammergau:null,Oberasbach:null,Oberau:null,Oberaudorf:null,"Oberbettingen-Hillesheim":null,Oberbillig:null,Oberbimbach:null,Oberboihingen:null,Oberbrechen:null,Oberdachstetten:null,"Oberderdingen-Flehingen Industrie":null,Oberelchingen:null,Oberelsungen:null,Obererbach:null,Oberesslingen:null,Oberferrieden:null,Obergimpern:null,Obergries:null,Obergriesbach:null,Obergrunstedt:null,Oberhaid:null,"Oberharmersbach Dorf":null,"Oberharmersbach-Riersbach":null,"Oberhausen Hbf":null,"Oberhausen-Holten":null,"Oberhausen-Osterfeld Süd":null,"Oberhausen-Osterfeld Süd":null,"Oberhausen-Sterkrade":null,"Oberhof(Thür)":null,Oberholz:null,Oberkirch:null,"Oberkirch-Köhlersiedlung":null,Oberkochen:null,Oberkotzau:null,Oberkrozingen:null,Oberlahnstein:null,Oberlauscha:null,Oberlenningen:null,Oberlichtenau:null,Oberlindhart:null,Oberlinxweiler:null,Obermaubach:null,Obermohr:null,Obernau:null,"Obernburg-Elsenfeld":null,"Oberndorf(Neckar)":null,"Oberndorf(Wittgenstein)":null,"Obernhof(Lahn)":null,Oberoderwitz:null,"Oberoderwitz Oberdorf":null,Oberottmarshausen:null,Oberrohn:null,Oberrothenbach:null,Oberrotweil:null,"Oberröblingen":null,Oberschefflenz:null,"Oberschleißheim":null,Oberschlottwitz:null,Obersdorf:null,Obersinn:null,Oberstaufen:null,Oberstdorf:null,Obertraubling:null,"Obertshausen(Kr Of)":null,Obertsrot:null,"Oberursel(Taunus)":null,"Oberursel-Stierstadt":null,"OberurselWeißkirchen/Steinbach":null,Obervogelgesang:null,Oberweimar:null,"Oberweißbach-Deesbach":null,Oberwerrn:null,Oberwesel:null,Oberwinden:null,Oberwinter:null,Oberzell:null,Oberzissen:null,"Oberöwisheim":null,Obing:null,Obstfelderschmiede:null,Ochenbruck:null,Ochsenfurt:null,Ochsenhausen:null,Ochtmersleben:null,Ochtrup:null,Ockenheim:null,"Odenheim Bf":null,"Odenheim West":null,Oderin:null,Oebisfelde:null,Oederan:null,Oegeln:null,Oehna:null,Oelde:null,"Oelsnitz Bahnhofstraße":null,"Oelsnitz(Erzgeb)":null,"Oelsnitz(Vogtl)":null,Oerel:null,Oerlenbach:null,Oerlinghausen:null,Oertzenhof:null,Oesede:null,"Oestrich-Winkel":null,"Oettingen(Bay)":null,Oeventrop:null,Offenau:null,"Offenbach(Main) Kaiserlei":null,"Offenbach(Main) Ledermuseum":null,"Offenbach(Main) Marktplatz":null,"Offenbach(Main)Hbf":null,"Offenbach(Main)Ost":null,"Offenbach-Bieber":null,"Offenbach-Waldhof":null,Offenburg:null,"Offenburg Kreisschulzentrum":null,Offenhausen:null,"Offensen(Kr North)":null,Offingen:null,Oftersheim:null,Ohlstadt:null,Okarben:null,Oker:null,Olbernhau:null,"Olbernhau West":null,"Olbernhau-Grünthal":null,"Olbersdorf Niederdorf":null,"Olbersdorf Oberdorf":null,"Olbersleben-Ellersleben":null,Olching:null,"Oldenburg(Holst)":null,"Oldenburg(Oldb)":null,"Oldenburg-Wechloy":null,"Oldenbüttel":null,Oldentrup:null,Olpe:null,Olsberg:null,"Olsbrücken":null,Opladen:null,Opladen:null,Oppenau:null,"Kiel-Oppendorf":null,Oppenheim:null,"Oppenweiler(Württ)":null,Oppurg:null,"Oranienbaum(Anh)":null,Oranienburg:null,"Orlamünde":null,Orschweier:null,Ortrand:null,Oschatz:null,"Oschatz Körnerstr":null,"Oschatz Lichtstr":null,"Oschatz Südbf":null,"Oschersleben(Bode)":null,"Osnabrück Altstadt":null,"Osnabrück Hbf":null,"Osnabrück Hbf":null,"Osnabrück Hbf":null,"Osnabrück-Sutthausen":null,Ostbevern:null,Osterburg:null,Osterburken:null,"Osterhofen(Nby)":null,"Osterhofen(Oberbay)":null,"Osterholz-Scharmbeck":null,"Ostermünchen":null,Osternienburg:null,"Osterode am Harz Leege":null,"Osterode am Harz Mitte":null,Ostersode:null,Osterspai:null,Osterstedt:null,Osterteich:null,Osterwald:null,Osterweddingen:null,"Ostheim v Rhön":null,"Ostheim(Kr Hanau)":null,"Ostheim(b Butzbach)":null,Osthofen:null,"Ostrach Bahnhof":null,Ostrau:null,"Ostseebad Binz":null,"Ostseebad Kühlungsborn Mitte":null,"Ostseebad Kühlungsborn Ost":null,"Ostseebad Kühlungsborn West":null,Ottbergen:null,Ottenau:null,"Ottendorf(Mittweida)":null,"Ottendorf-Okrilla Hp":null,"Ottendorf-Okrilla Nord":null,"Ottendorf-Okrilla Süd":null,"Ottenhofen(Oberbay)":null,"Ottenhofen-Bergel":null,"Ottenhöfen":null,"Ottenhöfen West":null,Ottensoos:null,Otterfing:null,Otterndorf:null,"Ottersberg(Han)":null,Otterwisch:null,Otting:null,"Otting-Weilheim":null,Ottobrunn:null,"Ottweiler(Saar)":null,"Otzberg Lengfeld":null,Otze:null,"Ovelgünne":null,Overath:null,"Owen(Teck)":null,Owschlag:null,"Oy-Mittelberg":null,"Oßmannstedt":null,"Paderborn Hbf":null,"Paderborn Kasseler Tor":null,"Paderborn Nord":null,"Paderborn-Schloss Neuhaus":null,Paffendorf:null,Paindorf:null,Palzem:null,Pankofen:null,Pansdorf:null,"Papenburg(Ems)":null,Papendorf:null,"Papiermühle(Stadtr)":null,Pappenheim:null,Parchim:null,Parkentin:null,Parsberg:null,Partenstein:null,Pasewalk:null,"Pasewalk Ost":null,"Passau Hbf":null,"Passow(Uckermark)":null,Patersdorf:null,Paulinenaue:null,Paulinenaue:null,Paulinzella:null,Pechbrunn:null,"Peenemünde":null,Pegau:null,Pegnitz:null,Peine:null,"Peiting Nord":null,"Peiting Ost":null,"Peitz Ost":null,"Peiß":null,"Peißen":null,"Peißenberg":null,"Peißenberg Nord":null,Penzberg:null,Perkam:null,Perl:null,Perleberg:null,Petergrube:null,Petersaurach:null,"Petersaurach Nord":null,"Petershagen Nord":null,"Petershagen(Uckerm)":null,"Petershagen-Lahde":null,"Petershausen(Obb)":null,Petersroda:null,Pfaffenhain:null,Pfaffenhausen:null,"Pfaffenhofen(Ilm)":null,Pfalzel:null,Pfarrkirchen:null,Pfeddersheim:null,Pfettrach:null,Pflaumloch:null,"Pforzheim Hbf":null,"Pforzheim Maihälden":null,"Pforzheim-Weißenstein":null,Pfreimd:null,"Pfronten-Ried":null,"Pfronten-Steinach":null,"Pfronten-Weißbach":null,Pfullendorf:null,Pfungstadt:null,"Pfäffingen":null,"Philippsburg(Baden)":null,Philippshagen:null,Philippsheim:null,Piding:null,Pillgram:null,Pinneberg:null,Pinneberg:null,"Pinnow(Uckermark)":null,Pinzberg:null,Pirk:null,"Pirmasens Hbf":null,"Pirmasens Nord":null,Pirna:null,"Pirna-Copitz":null,"Pirna-Copitz Nord":null,Pittenhart:null,Plaaz:null,Plaidt:null,Planegg:null,"Plate(Meckl)":null,Plattling:null,"Plaue(Thür)":null,"Plauen(Vogtl) Mitte":null,"Plauen(Vogtl) ob Bf":null,"Plauen(Vogtl)-Straßberg":null,"Plauen(Vogtl)West":null,Pleinfeld:null,Plessa:null,Plettenberg:null,Plochingen:null,"Plön":null,"Plüderhausen":null,"Plüschow":null,"Pockau-Lengefeld":null,Pocking:null,Poggenhagen:null,Poikam:null,Poing:null,Pommelsbrunn:null,"Pommern(Mosel)":null,Pommritz:null,Ponitz:null,Poppenhausen:null,"Porschdorf(Pirna)":null,Porstendorf:null,"Porta Westfalica":null,"Porz(Rhein)":null,"Porz(Rhein)":null,"Porz-Wahn":null,Posewald:null,Posewald:null,Possenhofen:null,"Postbauer-Heng":null,"Potsdam Charlottenhof":null,"Potsdam Griebnitzsee":null,"Potsdam Hbf":null,"Potsdam Medienstadt Babelsberg":null,"Potsdam Park Sanssouci":null,"Potsdam Pirschheide":null,"Potsdam-Babelsberg":null,"Potsdam-Rehbrücke":null,Praest:null,Pratau:null,Preetz:null,"Premnitz Nord":null,"Premnitz Zentrum":null,Prenzlau:null,Pressath:null,"Pressig-Rothenkirchen":null,Pretzfeld:null,"Pretzier(Altm)":null,Pretzsch:null,"Preußen":null,Priemerburg:null,"Prien a Chiemsee":null,Priestewitz:null,Priort:null,Prisdorf:null,Prittitz:null,Pritzerbe:null,Pritzier:null,Pritzwalk:null,"Pritzwalk Hainholz":null,"Pritzwalk West":null,Probsteierhagen:null,Probstzella:null,Profen:null,Prora:null,"Prora Ost":null,Prosselsheim:null,"Prödel":null,"Prösen":null,"Prösen Ost":null,"Prösen West":null,Puchheim:null,Pulheim:null,Pullach:null,"Pulling(b Freising)":null,Pulsnitz:null,"Pulsnitz Süd":null,Puschendorf:null,Putbus:null,Puttgarden:null,Putzkau:null,"Pölchow":null,"Pölling":null,"Pönitz(Holst)":null,"Pönitz(Leipzig)":null,"Pösing":null,"Pößneck ob Bf":null,"Pößneck unt Bf":null,"Quadrath-Ichendorf":null,"Quakenbrück":null,Quedlinburg:null,"Quedlinburg-Quarmbeck":null,Quelle:null,Quelle:null,Quelle:null,"Quelle-Kupferheide":null,Querfurt:null,Quickborn:null,"Quickborn Süd":null,"Quickborner Straße":null,Quierschied:null,Quint:null,Rabenau:null,"Rackith(Elbe)":null,"Rackwitz(Leipzig)":null,Radbruch:null,Raddusch:null,Radeberg:null,"Radebeul Ost":null,"Radebeul Ost":null,"Radebeul-Kötzschenbroda":null,"Radebeul-Naundorf":null,"Radebeul-Weintraube":null,"Radebeul-Zitzschewig":null,Radeburg:null,Radersdorf:null,Radis:null,"Radldorf(Niederbay)":null,Radolfzell:null,"Raestrup-Everswinkel":null,Raguhn:null,"Rahden(Kr Lübbecke)":null,Rain:null,Raindorf:null,Raisdorf:null,Raisdorf:null,Raisting:null,Raitersaich:null,Rakow:null,"Rambin(Rügen)":null,Ramerberg:null,Rammelsbach:null,"Rammingen(Bay)":null,"Rammingen(Württ)":null,"Ramsbach Birkhof":null,"Ramsbach Höfle":null,Ramsberg:null,Ramsen:null,Ramsenthal:null,Ramstein:null,Rangendingen:null,Rangsdorf:null,Ranstadt:null,Rastatt:null,"Rastatt Beinle":null,Rastede:null,Rastow:null,Rathenow:null,"Rathmannsdorf(Kr Pirna)":null,"Ratingen Ost":null,Ratzeburg:null,Raubling:null,"Rauenstein(Thür)":null,"Raumland-Markhausen":null,"Raumünzach":null,Raun:null,Raunheim:null,Ravensburg:null,"Rebdorf-Hofmühle":null,Rech:null,Rechenberg:null,Rechtenstein:null,Rechterfeld:null,Reckendorf:null,Reckenfeld:null,"Recklinghausen Hbf":null,"Recklinghausen Süd":null,Reckweilerhof:null,Reddelich:null,Rednitzhembach:null,"Redwitz(Rodach)":null,Regen:null,"Regensburg Hbf":null,"Regensburg-Burgweinting":null,"Regensburg-Prüfening":null,"Regensburg-Prüfening":null,Regenstauf:null,"Regis-Breitingen":null,Rehau:null,"Rehfeld(Falkenberg)":null,Rehfelde:null,Rehna:null,Rehweiler:null,"Reichelsdorfer Keller":null,"Reichelsheim(Wett)":null,"Reichenau(Baden)":null,"Reichenbach(Fils)":null,"Reichenbach(Oberlausitz)":null,"Reichenbach(Vogtl) ob Bf":null,"Reichenbach(b. Ettlingen)":null,"Reichenberg(Unterfr)":null,Reichenschwand:null,Reichersbeuern:null,"Reichertshausen(Ilm)":null,Reicholzheim:null,"Reifland-Wünschendorf":null,Reihen:null,Reil:null,Reilsheim:null,Reinbek:null,"Reinfeld(Holst)":null,"Reinhardsbrunn-Friedrichroda":null,"Reinheim(Odenw)":null,"Reinsbüttel":null,"Reinsdorf(Artern)":null,"Reinsdorf(bei Nebra)":null,Reinstetten:null,"Reisen(Hess)":null,"Reiskirchen(Kr Gi)":null,Reken:null,Remagen:null,"Remscheid Hbf":null,"Remscheid-Güldenwerth":null,"Remscheid-Lennep":null,"Remscheid-Lüttringhausen":null,Renchen:null,Rendsburg:null,Renningen:null,"Renningen Süd":null,Rennsteig:null,Rentrisch:null,Rentweinsdorf:null,Rentwertshausen:null,"Rentzschmühle":null,"Rethen(Leine)":null,"Retzbach-Zellingen":null,Reurieth:null,"Reuterstadt Stavenhagen":null,"Reuth(b Erbendorf)":null,"Reuth(b Plauen,Vogtl)":null,"Reutlingen Hbf":null,"Reutlingen West":null,"Reutlingen-Betzingen":null,"Reutlingen-Sondelfingen":null,"Reußen":null,Rhade:null,"Rheda-Wiedenbrück":null,Rheinbach:null,"Rheinbach Römerkanal":null,"Rheinberg(Rheinl)":null,Rheinbrohl:null,Rheine:null,"Rheine-Mesum":null,"Rheinfelden(Baden)":null,Rheinhausen:null,"Rheinhausen Ost":null,"Rheinsberg(Mark)":null,Rheinsheim:null,Rheinweiler:null,"Rheinzabern Alte Römerstraße":null,"Rheinzabern Bf":null,"Rheinzabern Rappengasse":null,Rhens:null,"Rheydt Hbf":null,"Rheydt Hbf":null,"Rheydt-Odenkirchen":null,"Rhöndorf":null,"Ribnitz-Damgarten Ost":null,"Ribnitz-Damgarten West":null,"Richen(b Eppingen)":null,Rickling:null,Ried:null,Riederau:null,Riedlingen:null,Riedrode:null,"Riedstadt-Goddelau":null,"Riedstadt-Wolfskehlen":null,"Riegel am Kaiserstuhl Ort":null,"Riegel-Malterd.NE":null,"Riegel-Malterdingen":null,Riehen:null,"Riehen Niederholz":null,Rieneck:null,Riesa:null,Rieschweiler:null,Rieseby:null,Rieste:null,Riestedt:null,"Rietheim(Württ)":null,Rietschen:null,Rimbach:null,"Ringleben-Gebesee":null,Ringsheim:null,Rinkerode:null,Rinklingen:null,Rinnthal:null,Rinteln:null,Rippberg:null,Ritschenhausen:null,Ritterhude:null,Rockenhausen:null,Rodalben:null,"Rodenbach(Dillkr)":null,"Rodenbach(b Hanau)":null,"Rodenkirchen(Oldb)":null,Rodewisch:null,"Rodgau-Dudenhofen":null,"Rodgau-Hainhausen":null,"Rodgau-Jügesheim":null,"Rodgau-Nieder Roden":null,"Rodgau-Rollwald":null,"Rodgau-Weiskirchen":null,"Rodheim v d Höhe":null,Roding:null,Rodleben:null,Roggentin:null,"Rohr(Thür)":null,"Rohrbach(Ilm)":null,"Rohrbach(Oberbay)":null,"Rohrbach(Pfalz)":null,"Rohrbach(Saar)":null,Rohrenfeld:null,Roigheim:null,Roisdorf:null,"Roitzsch(Bitterf)":null,Rolandseck:null,Rollhofen:null,Rommelshausen:null,Rommerskirchen:null,"Ronneburg(Thür)":null,Ronnenberg:null,Ronshausen:null,"Rosbach v d Höhe":null,"Rosbach(Sieg)":null,"Rosenau(b Grafenau)":null,"Rosenberg(Baden)":null,"Rosendahl-Holtwick":null,Rosenheim:null,"Rosenheim Hochschule":null,Rosenwinkel:null,"Rostock Hbf":null,"Rostock Holbeinplatz":null,"Rostock Parkstraße":null,"Rostock Thierfelder Str.":null,"Rostock-Bramow":null,"Rostock-Evershagen":null,"Rostock-Kassebohm":null,"Rostock-Lichtenhagen":null,"Rostock-Lütten Klein":null,"Rostock-Marienehe":null,"Rostock-Torfbrücke":null,"Rot am See":null,"Rot-Malsch":null,"Rotenbach(Enz)":null,"Rotenburg a.d. Fulda":null,"Rotenburg(Wümme)":null,Rotenhain:null,Roth:null,"Rothenburg ob der Tauber":null,"Rothenbürg":null,"Rothenstein(Saale)":null,"Rott(Inn)":null,Rottenacker:null,Rottenbach:null,"Rottenburg(Neckar)":null,Rottendorf:null,Rottershausen:null,Rottweil:null,"Rottweil Göllsdorf":null,"Rottweil Neufra":null,"Rottweil Saline":null,"Roßbach(Pfalz)":null,"Roßla":null,"Roßlau(Elbe)":null,"Roßtal":null,"Roßtal Wegbrücke":null,Rudersberg:null,"Rudersberg Nord":null,"Rudersberg-Oberndorf":null,"Rudersdorf(Siegen)":null,"Rudolstadt(Thür)":null,"Rudolstadt-Schwarza":null,Ruhland:null,"Ruhlsdorf-Zerpenschleuse":null,Ruhmannsfelden:null,Ruhpolding:null,Ruhstorf:null,Rumeln:null,Rummenohl:null,Runkel:null,"Ruppertsgrün":null,Rupprechtstegen:null,Ruschberg:null,Ruschwedel:null,Rutesheim:null,Ruthenbeck:null,"Rätzlingen":null,"Röblingen am See":null,"Rödental":null,"Rödental Mitte":null,"Rödermark-Ober Roden":null,"Rödermark-Urberach":null,"Rödlitz-Hohndorf":null,"Röhrmoos":null,"Röhrnbach":null,"Rönshausen":null,"Röntgental":null,"Röslau":null,"Rösrath":null,"Rösrath-Stümpen":null,"Röt":null,"Rötenbach(Baden)":null,"Rötgesbüttel":null,"Röthenbach(Allgäu)":null,"Röthenbach(Oberpf)":null,"Röthenbach(Pegnitz)":null,"Röthenbach-Seespitze":null,"Röthenbach-Steinberg":null,"Rövershagen":null,"Rückersbacher Schlucht":null,"Rückersdorf":null,"Rückersdorf(Mfr)":null,"Rüdesheim(Rhein)":null,"Rüdnitz":null,"Rülzheim Bf":null,"Rülzheim Freizeitzentrum":null,"Rümmingen":null,"Ründeroth":null,"Rüsselbach":null,"Rüsselsheim":null,"Rüsselsheim Opelwerk":null,"Saal(Donau)":null,"Saalburg(Taunus)":null,"Saalfeld(Saale)":null,"Saarbrücken Hbf":null,"Saarbrücken Ost":null,"Saarbrücken-Burbach":null,"Saarburg(Bz Trier)":null,"Saarhölzbach":null,"Saarlouis Hbf":null,Saarmund:null,Saasen:null,Saatel:null,"Sachsen(b Ansbach)":null,"Sachsendorf(Calbe)":null,"Sachsenhausen(Nordb)":null,Sachsenheim:null,Sagard:null,Sagehorn:null,Salach:null,Salem:null,Sallach:null,Salmtal:null,Salzbergen:null,"Salzgitter-Bad":null,"Salzgitter-Immendorf":null,"Salzgitter-Lebenstedt":null,"Salzgitter-Ringelheim":null,"Salzgitter-Thiede":null,"Salzgitter-Watenstedt":null,Salzkotten:null,Salzwedel:null,Samtens:null,"Sand(Niederbay)":null,Sande:null,Sandebeck:null,Sanderbusch:null,"Sandersleben(Anh)":null,"Sandförde":null,"Sandhagen(b Bad Dob)":null,Sandkrug:null,Sandwehle:null,Sangerhausen:null,"Sanitz(b Rostock)":null,Sanssouci:null,Sarnau:null,Sarnow:null,Sarstedt:null,"Sasbach am Kaiserstuhl":null,Sassenroth:null,Sassnitz:null,Satteldorf:null,Satzvey:null,Sauerlach:null,Saulgrub:null,Saulheim:null,"Schafbrücke":null,Schaftlach:null,"Schaidt(Pfalz)":null,Schalchen:null,Schalkau:null,"Schalkau Mitte":null,"Schalksmühle":null,Schalkstetten:null,Schallstadt:null,Schameder:null,Schandelah:null,Scharbeutz:null,Scharfenstein:null,Scharmede:null,Scharstorf:null,Schechen:null,"Scheeßel":null,"Scheidt(Saar)":null,Schelklingen:null,Schemmerberg:null,Schenkenzell:null,Scheppach:null,Scherfede:null,"Scheuerfeld(Sieg)":null,Scheven:null,Schieder:null,Schierbrok:null,Schierke:null,Schierling:null,Schierstedt:null,Schifferstadt:null,"Schifferstadt Süd":null,Schiffweiler:null,Schiltach:null,"Schiltach Mitte":null,Schimborn:null,"Schirgiswalde-Kirschau":null,Schirnding:null,Schkeuditz:null,"Schkeuditz West":null,Schkopau:null,"Schladen(Harz)":null,"Schladern(Sieg)":null,"Schlatt(Hohenz)":null,Schlechtbach:null,Schleife:null,Schleswig:null,Schliengen:null,"Schlierbach(Schwalm-Eder-Kr.)":null,Schliersee:null,"Schloß Holte":null,Schluchsee:null,"Schlüchtern":null,Schmachtenhagen:null,Schmalkalden:null,"Schmalkalden-Fachhochschule":null,Schmalnau:null,Schmidtheim:null,Schmiechen:null,"Schmiechen Albbahn":null,"Schmiechen(Schwab)":null,"Schmilka-Hirschmühle":null,Schmollensee:null,"Schmölln(Ol)":null,"Schmölln(Thür)":null,Schnabelwaid:null,"Schnaittach Markt":null,"Schneeberg im Odenwald":null,"Schneeberg(Mark)":null,Schnega:null,Schneidhain:null,Schnelldorf:null,Schneverdingen:null,Schney:null,"Schnitzmühle":null,"Schoden-Ockfen":null,"Schondorf(Bay)":null,Schongau:null,Schonungen:null,Schopfheim:null,"Schopfheim West":null,"Schopfloch(b Freudenstadt)":null,Schopp:null,Schorndorf:null,"Schorndorf-Hammerschlag":null,"Schortens-Heidmühle":null,Schrezheim:null,Schrobenhausen:null,Schrozberg:null,Schutzbach:null,Schwaan:null,Schwabach:null,"Schwabach-Limbach":null,"Schwabhausen(b Dachau)":null,"Schwabmünchen":null,Schwabsberg:null,Schwaig:null,"Schwaigern Ost":null,"Schwaigern(Württ)":null,"Schwaigern(Württ) West":null,Schwaikheim:null,"Schwalbach(Taunus)Limes":null,"Schwalbach(Taunus)Nord":null,Schwallungen:null,"Schwalmstadt-Wiera":null,Schwandorf:null,Schwanheide:null,Schwante:null,Schwarmstedt:null,Schwarzburg:null,"Schwarzenbach(Saale)":null,"Schwarzenbach(b Pressath)":null,Schwarzenbek:null,Schwarzenberg:null,"Schwarzenberg Hp":null,"Schwarzenberg(Erzg)":null,"Schwarzenberg-Neuwelt":null,"Schwarzenfeld(Opf)":null,"Schwarzheide Ost":null,Schwarzkollm:null,"Schwedt(Oder)":null,"Schwedt(Oder)Mitte":null,"Schweich(DB)":null,Schweighofen:null,Schweikershain:null,"Schweinfurt Hbf":null,"Schweinfurt Mitte":null,"Schweinfurt Stadt":null,"Schweinsburg-Culten":null,Schweinsdorf:null,Schwelm:null,"Schwelm West":null,"Schwenningen(Bay)":null,"Schwenningen(Neckar)":null,Schwenzin:null,"Schweppenburg-Heilbrunnen":null,"Schwerin Hbf":null,"Schwerin Mitte":null,"Schwerin Süd":null,"Schwerin-Görries":null,"Schwerin-Görries":null,"Schwerin-Lankow":null,"Schwerin-Margaretenhof":null,"Schwerin-Warnitz":null,"Schwerin-Wüstmark":null,"Schwerte(Ruhr)":null,"Schweta Bf":null,"Schweta Gasth":null,Schwetzingen:null,Schwieberdingen:null,Schwindegg:null,"Schwäbisch Gmünd":null,"Schwäbisch Hall":null,"Schwäbisch Hall-Hessental":null,"Schwörstadt":null,"Schöllkrippen":null,"Schömberg Stausee":null,"Schömberg(b Balingen)":null,"Schöna":null,"Schönau(Hörsel)":null,"Schönberg(Holstein)":null,"Schönberg(Meckl)":null,"Schönberg(Vogtl)":null,"Schönberger Strand":null,"Schönborn(Doberl)":null,"Schönebeck Süd":null,"Schönebeck(Elbe)":null,"Schönebeck-Bad Salzelmen":null,"Schönebeck-Felgeleben":null,"Schönebeck-Frohse":null,"Schöneck(Vogtl)":null,"Schöneck(Vogtl) Ferienpark":null,"Schöneck-Büdesheim":null,"Schöneck-Kilianstädten":null,"Schöneck-Oberdorfelden":null,"Schönerlinde":null,"Schönewörde":null,"Schönfließ(Mark) Dorf":null,"Schönfließ(b Oranienburg)":null,"Schöngeising":null,"Schönhausen(Elbe)":null,"Schönkirchen Bf":null,"Schönmünzach":null,"Schönow(Angerm)":null,"Schönstedt":null,"Schönwald(Oberfr)":null,"Schönwalde(Barnim)":null,"Schönwalde(Spreewald)":null,"Schöppenstedt":null,"Schötmar":null,"Schülldorf":null,"Schüptitz":null,"Schüttorf":null,"Sebnitz(Sachs)":null,Sechshelden:null,Sechtem:null,Seckach:null,Seddin:null,"Sedlitz Ost":null,"Seebach(Mühlhausen)":null,"Seebad Ahlbeck":null,"Seebad Heringsdorf":null,Seebergen:null,Seebrugg:null,"Seefeld(Mark)":null,"Seefeld-Hechendorf":null,Seeg:null,Seegefeld:null,"Seehausen(Altm)":null,"Seehausen(Uckermark)":null,"Seeleiten-Berggeist":null,"Seelow(Mark)":null,"Seelow-Gusow":null,Seelvitz:null,Seelze:null,Seerhausen:null,Seesen:null,Seeshaupt:null,"Sehlem(Kr Wittlich)":null,Sehma:null,Sehnde:null,Seiboldsdorf:null,Seifersdorf:null,Seifhennersdorf:null,Seitschen:null,"Selb Nord":null,"Selb Stadt":null,"Selb-Plößberg":null,Selbitz:null,Selhausen:null,"Seligenstadt Mainschleifenbahn":null,"Seligenstadt(Hess)":null,"Seligenstadt(b Würzburg)":null,"Sellin(Rügen) Ost":null,"Sellin(Rügen) West":null,Sellstedt:null,Selm:null,"Selm-Beifang":null,Seltendorf:null,Senden:null,Senftenberg:null,Sennelager:null,Sennestadt:null,Sennfeld:null,Serams:null,Serrig:null,Sersheim:null,Seubersdorf:null,Seulberg:null,Seulbitz:null,Seybothenreuth:null,"Siebeldingen-Birkweiler":null,Siedlinghausen:null,"Siegburg Bahnhof":null,"Siegburg/Bonn":null,Siegelsbach:null,Siegelsdorf:null,Siegen:null,"Siegen-Geisweid":null,"Siegen-Weidenau":null,Siegsdorf:null,Sierksdorf:null,Siersburg:null,Siershahn:null,Siglingen:null,Sigmaringen:null,Sigmaringendorf:null,Sigmaringendorf:null,Silbach:null,Silberhausen:null,"Silberhütte NE":null,"Silberstraße":null,"Simbach(Inn)":null,"Simmelsdorf-Hüttenbach":null,Simtshausen:null,Sindelfingen:null,Sindorf:null,Sindorf:null,"Singen Industriegebiet":null,"Singen Landesgartenschau":null,"Singen(Hohentwiel)":null,"Singen(Thür)":null,Singlis:null,Sinn:null,"Sinsheim Museum/Arena":null,"Sinsheim(Elsenz) Hbf":null,Sinzheim:null,"Sinzheim Nord":null,"Sinzig(Rhein)":null,Sinzing:null,Sipplingen:null,"Sitzendorf-Unterweißbach":null,Soest:null,Sohl:null,Sohland:null,"Solingen Grünewald":null,"Solingen Hbf":null,"Solingen Mitte":null,"Solingen Vogelpark":null,"Solingen-Schaberg":null,Sollstedt:null,Solms:null,Solnhofen:null,Solpke:null,"Soltau Nord":null,"Soltau(Han)":null,Soltendieck:null,Sondern:null,Sondernach:null,Sondernheim:null,Sondershausen:null,"Sonneberg(Thür)Hbf":null,"Sonneberg(Thür)Nord":null,"Sonneberg(Thür)Ost":null,"Sonneberg(Thür)West":null,"Sontheim(Schwab)":null,"Sontheim-Brenz":null,Sonthofen:null,Sontra:null,Sophienhof:null,Sorge:null,Sottrum:null,Soyen:null,Spaichingen:null,"Spaichingen Mitte":null,Sparrieshoop:null,Spay:null,Spechtritz:null,Speele:null,Speicher:null,Speikern:null,"Speyer Hbf":null,"Speyer Nord-West":null,Spich:null,Spiegelau:null,Spielberg:null,Sponholz:null,Spornitz:null,Spremberg:null,"Sprendlingen(Rheinhess)":null,"Sprendlingen(Rheinhess)":null,"Sprendlingen(Rheinhess)":null,Springe:null,"Sprötze":null,"St Alban":null,"St Augustin Markt":null,"St Egidien":null,"St Georgen(Schwarzw)":null,"St Goar":null,"St Goarshausen":null,"St Ilgen-Sandhausen":null,"St Ingbert":null,"St Koloman":null,"St Mang":null,"St Michaelisdonn":null,"St Ottilien":null,"St Thomas":null,"St Wendel":null,Stade:null,"Stadt Wehlen(Sachs)":null,Stadtallendorf:null,Stadthagen:null,Stadtilm:null,Stadtoldendorf:null,Stadtprozelten:null,Stadtroda:null,Staffel:null,Stahringen:null,Stambach:null,Stammbach:null,Stapelburg:null,Starnberg:null,"Starnberg Nord":null,Stauchitz:null,Staudernheim:null,Staufen:null,"Staufen Süd":null,"Staßfurt":null,"Stederdorf(Kr Uelzen)":null,Stegenwaldhaus:null,"Steilküste/Wittenbeck":null,"Stein(Traun)":null,Steina:null,"Steinach(Baden)":null,"Steinach(Thür)":null,"Steinach(Thür)Süd":null,"Steinach(b Rothenburg ob der Tauber)":null,Steinalben:null,"Steinau(Straße)":null,"Steinbach am Wald":null,"Steinbach-Hallenberg":null,Steinebach:null,Steinefrenz:null,Steinen:null,"Steinerne Renne":null,"Steinfeld(Oldb)":null,"Steinfeld(Pfalz)":null,"Steinfeld(Stendal)":null,"Steinfurt-Borghorst":null,"Steinfurt-Burgsteinfurt":null,"Steinfurt-Grottenkamp":null,"Steinhagen(Westf)":null,"Steinhagen(Westf) Bielef. Str.":null,"Steinhausen-Neuburg":null,"Steinheim(Main)":null,"Steinheim(Westf)":null,"Steinhöring":null,Steinpleis:null,Steinsfurt:null,Steinweiler:null,Steinwenden:null,"Steinwiesen Bf":null,Stelle:null,Stendal:null,"Stendal Vorbf":null,"Stendal-Stadtsee":null,Stenn:null,Sterbfritz:null,Sternfeld:null,"Sternhaus-Haferfeld":null,"Sternhaus-Ramberg":null,Sterzhausen:null,"Stetten (b. Haigerloch)":null,"Stetten am Heuchelberg":null,"Stetten(Donau)":null,"Stetten(Schwab)":null,"Stetten-Beinstein":null,"Stettfeld(Baden)":null,Stiege:null,"Stift Keppel-Allenbach":null,"Stockach NE":null,Stockau:null,Stockdorf:null,"Stockhausen(Lahn)":null,"Stockheim(Oberfr)":null,"Stockheim(Unterfr)":null,"Stockstadt(Main)":null,"Stockstadt(Rhein)":null,"Stolberg(Rheinl)Gbf":null,"Stolberg(Rheinl)Hbf":null,"Stolberg(Rheinl)Hbf":null,"Stolberg(Rheinl)Hbf Gl.27":null,"Stolberg(Rheinl)Hbf Gl.44":null,"Stolberg-Altstadt":null,"Stolberg-Mühlener Bahnhof":null,"Stolberg-Rathaus":null,"Stolberg-Schneidmühle":null,"Stollberg Schlachthofstraße":null,"Stollberg(Sachs)":null,Stolpen:null,Stommeln:null,"Storkow(Mark)":null,Storzingen:null,Stotternheim:null,"Stralsund Hbf":null,"Stralsund Rügendamm":null,"Stralsund-Grünhufe":null,"Strasburg(Uckerm)":null,Straubing:null,"Straubing-Ost":null,Strausberg:null,"Strausberg Nord":null,"Strausberg Stadt":null,"Strausberg-Hegermühle":null,"Straußfurt":null,"Straß-Moos":null,"Straßberg(Harz)":null,"Straßberg-Glasebach":null,"Straßberg-Winterlingen":null,"Straßkirchen":null,Strohkirchen:null,Strullendorf:null,"Struthütten":null,Stryck:null,Stubben:null,Stubbenfelde:null,Stubersheim:null,Stumsdorf:null,"Stuttgart Ebitzweg":null,"Stuttgart Feuersee":null,"Stuttgart Flughafen/Messe":null,"Stuttgart Hbf":null,"Stuttgart Hbf (tief)":null,"Stuttgart Neckarpark":null,"Stuttgart Nord":null,"Stuttgart Nord":null,"Stuttgart Nürnberger Str.":null,"Stuttgart Schwabstr.":null,"Stuttgart Stadtmitte":null,"Stuttgart Universität":null,"Stuttgart-Bad Cannstatt":null,"Stuttgart-Feuerbach":null,"Stuttgart-Feuerbach":null,"Stuttgart-Münster":null,"Stuttgart-Obertürkheim":null,"Stuttgart-Rohr":null,"Stuttgart-Sommerrain":null,"Stuttgart-Untertürkheim":null,"Stuttgart-Untertürkheim":null,"Stuttgart-Untertürkheim":null,"Stuttgart-Vaihingen":null,"Stuttgart-Zazenhausen":null,"Stuttgart-Zuffenhausen":null,"Stuttgart-Österfeld":null,"Stühlingen":null,"Stützerbach":null,"Subzin-Liessow":null,Suchsdorf:null,Suderburg:null,Suerhop:null,Suhl:null,"Suhl-Heinrichs":null,"Sukow(b Schwerin)":null,Sulmingen:null,"Sulz(Neckar)":null,"Sulzbach(Inn)":null,"Sulzbach(Main)":null,"Sulzbach(Murr)":null,"Sulzbach(Saar)":null,"Sulzbach(Saar)Altenwald":null,"Sulzbach(Taunus)":null,"Sulzbach(Taunus)Nord":null,"Sulzbach-Rosenberg":null,"Sulzbach-Rosenberg Hütte":null,Sulzberg:null,"Sulzfeld(Baden)":null,"Swisttal-Odendorf":null,Syke:null,Sylbach:null,Syrau:null,Sythen:null,"Sättelstädt":null,"Söllichau":null,"Söllingen Kapellenstraße":null,"Söllingen Reetzstr.":null,"Söllingen(b Karlsr)":null,"Söllingen(b Karlsr)":null,"Sömmerda":null,"Sörup":null,"Süderbrarup":null,"Süderdeich":null,"Süderlügum":null,"Sülstorf":null,"Sülzbach":null,"Sülzbach Schule":null,"Sülzenbrücken":null,"Sünching":null,"Süßen":null,Taben:null,Tacherting:null,"Tamm(Württ)":null,"Tangerhütte":null,"Tangermünde":null,"Tangermünde West":null,Tanndorf:null,Tanneneck:null,"Tannheim(Württ)":null,Tannroda:null,Tantow:null,Tapfheim:null,Tarp:null,Tating:null,"Taubenheim(Spree)":null,Tauberbischofsheim:null,Tauberfeld:null,"Taucha(Leipzig)":null,Taufkirchen:null,Tautenhain:null,Tegernsee:null,Teicha:null,Teichland:null,Teisendorf:null,Teisnach:null,"Teisnach Rohde&Schwarz":null,Telgte:null,Teltow:null,"Teltow Stadt":null,Temmels:null,Templin:null,"Templin Stadt":null,"Teningen-Mundingen":null,Teschenhagen:null,Teschow:null,Tessin:null,"Tessin West":null,Teterow:null,Teuchern:null,"Teufelsmühle":null,Teutschenthal:null,"Teutschenthal Ost":null,"Thale Hbf":null,"Thale Musestieg":null,"Thaleischweiler-Fröschen":null,"Thalfingen(b Ulm)":null,"Thalheim (b Oschatz)":null,"Thalheim(Erzgeb)":null,"Thann-Matzbach":null,"Thansüß":null,Tharandt:null,Thayngen:null,Theisbergstegen:null,"Theißen":null,Themar:null,"Thermalbad Wiesenbad":null,Thesdorf:null,"Thiergarten(Hohenz)":null,Thyrow:null,"Thüngersheim":null,"Thür":null,Tiebensee:null,Tiefenau:null,"Tiefenbach(b Passau)":null,"Tiefenbachmühle":null,"Tiengen(Hochrhein)":null,Timmendorferstrand:null,Titisee:null,"Tittmoning-Wiesmühl":null,Torgau:null,Torgelow:null,Tornesch:null,Tostedt:null,"Traben-Trarbach":null,Trabitz:null,"Trais-Horloff":null,Trasadingen:null,Trassenheide:null,Trassenmoor:null,Traundorf:null,Traunreut:null,Traunstein:null,"Traunstein Klinikum":null,Trebbin:null,"Treben-Lehma":null,Trebgast:null,"Trebitz(Elbe)":null,"Trebitz(Könnern)":null,"Trebnitz(Mark)":null,Trechtingshausen:null,"Treis-Karden":null,Treuchtlingen:null,Treuen:null,Treuenbrietzen:null,"Treuenbrietzen Süd":null,Treysa:null,Triangel:null,Triberg:null,Triebes:null,Triefenried:null,"Trier Hbf":null,"Trier Süd":null,Triesdorf:null,Triptis:null,"Trochtelfingen ALB-GOLD":null,"Trochtelfingen(Hohenz)":null,"Trochtelfingen(b Bopfingen)":null,Troisdorf:null,Trompet:null,Tromsdorf:null,"Trossingen Bahnhof":null,"Trossingen Bahnhof":null,"Trossingen Stadt":null,Trostberg:null,Tulling:null,Tuttlingen:null,"Tuttlingen Gänsäcker":null,"Tuttlingen Nord":null,"Tuttlingen Schulen":null,"Tuttlingen Zentrum":null,Tutzing:null,Twiste:null,Twistringen:null,"Töging(Inn)":null,"Tönning":null,"Töppeln":null,"Tübingen Hbf":null,"Tübingen West":null,"Tübingen-Derendingen":null,"Tübingen-Lustnau":null,"Türkenfeld":null,"Türkheim(Bay)Bf":null,"Türkismühle":null,"Tüßling":null,Ubbedissen:null,"Ubstadt Ort":null,"Ubstadt Salzbrunnenstr":null,"Ubstadt Uhlandstr.":null,"Ubstadt-Weiher":null,Uchtspringe:null,Uder:null,Uebigau:null,"Ueckermünde":null,"Ueckermünde Stadthafen":null,Uelzen:null,Uffenheim:null,"Uffing a Staffelsee":null,Uhingen:null,"Uhldingen-Mühlhofen":null,Uhlerborn:null,"Uhlstädt":null,Uhsmannsdorf:null,Ulbersdorf:null,"Ulm Hbf":null,"Ulm Ost":null,"Ulm-Donautal":null,"Ulm-Söflingen":null,"Ulzburg Süd":null,"Umrathshausen Bf":null,"Umrathshausen Ort":null,Unadingen:null,Undorf:null,Unfriedsdorf:null,Ungedanken:null,Unkel:null,Unna:null,"Unna West":null,"Unna-Königsborn":null,"Unnau-Korb":null,Unterammergau:null,Unterasbach:null,Unteraschau:null,Unterelchingen:null,"Unterföhring":null,Untergimpern:null,Untergrainau:null,Untergriesheim:null,Untergrombach:null,Unterhaching:null,Unterharmersbach:null,"Unterhausen(Bay)":null,Unterheckenhofen:null,"Unterjesingen Mitte":null,"Unterjesingen Sandäcker":null,Unterkochen:null,Unterlemnitz:null,Unterlenningen:null,Unterloquitz:null,"Unterlüß":null,"Untermaubach-Schlagstein":null,"Untermaßfeld":null,Unterneudorf:null,Unterreichenbach:null,"Unterschleißheim":null,"Untersteinach(Bayr)":null,"Untersteinach(b Stadtsteinach)":null,Untersulzbach:null,Unterwellenborn:null,Unterwiesenthal:null,"Unteröwisheim Bf":null,"Unteröwisheim M.-Luther-Str.":null,Uphusum:null,"Urbach(b Schorndorf)":null,Urft:null,Urmersbach:null,Urmitz:null,"Urmitz Rheinbrücke":null,Urschalling:null,Urspring:null,"Usch-Zendscheid":null,Usingen:null,Uslar:null,Usseln:null,Utting:null,Utzedel:null,Vach:null,Vachdorf:null,Vachendorf:null,Vahldorf:null,"Vaihingen(Enz)":null,Vallendar:null,"Varel(Oldb)":null,Vastorf:null,Vaterstetten:null,Vechelde:null,Vechta:null,"Vechta-Stoppelmarkt":null,Vehlefanz:null,Veilsdorf:null,"Veitshöchheim":null,"Velbert Rosenhügel":null,"Velbert-Langenberg":null,"Velbert-Neviges":null,
-"Velbert-Nierenhof":null,"Velden(b Hersbruck)":null,Velgast:null,"Vellmar-Niedervellmar":null,"Vellmar-Obervellmar":null,"Vellmar-Osterberg/EKZ":null,"Velten(Mark)":null,Ventschow:null,"Verden(Aller)":null,Veringendorf:null,Veringenstadt:null,Vernawahlshausen:null,Vetschau:null,"Vettweiß":null,Viechtach:null,Vienenburg:null,"Vierenstraße":null,"Vierkirchen-Esterhofen":null,Viernau:null,Viersen:null,Vieselbach:null,"Villingen(Schwarzw)":null,"Villingen-Schwenningen Eisstadion":null,"Villingen-Schwenningen Hammerstatt":null,Villmar:null,Vilsbiburg:null,Vilseck:null,"Vilshofen(Niederbay)":null,Vinzelberg:null,"Visselhövede":null,Vlotho:null,"Voerde(Niederrhein)":null,"Vogelsang(Gransee)":null,Vogelweh:null,Vohburg:null,"Voigtsgrün":null,Voigtstedt:null,Voldagsen:null,"Volkach-Astheim":null,Volkmarsen:null,Volkringhausen:null,Volpriehausen:null,Vorhop:null,Vormwald:null,"Vormwald Dorf":null,"Vorra(Pegnitz)":null,"Voßloch":null,"Vöhl-Ederbringhausen":null,"Vöhl-Herzhausen":null,"Vöhl-Schmittlotheim":null,"Vöhl-Thalitter":null,"Vöhringen":null,"Vöhrum":null,"Völklingen":null,"Völksen/Eldagsen":null,"Wabern(Bz Kassel)":null,"Wachenheim(Pfalz)":null,"Wachenheim-Mölsheim":null,Wackershofen:null,Waffenbrunn:null,Waggonfabrik:null,"Waghäusel":null,Waging:null,"Wahlbach(Kr Siegen)":null,Wahlheim:null,Wahlitz:null,Wahlstedt:null,Wahlwies:null,Wahrenholz:null,Waiblingen:null,Waibstadt:null,Waigolshausen:null,Wakendorf:null,Walddrehna:null,"Waldenburg(Württ)":null,Waldershof:null,Waldfischbach:null,"Waldhausen(b Geislingen)":null,"Waldhausen(b Schorndorf)":null,Waldheim:null,Waldkirch:null,"Waldkirchen(Erzgeb)":null,"Waldkirchen(Niederbay.)":null,Waldkraiburg:null,"Waldmünchen":null,Waldshut:null,Waldshut:null,"Walhausen(Saar)":null,"Walheim(Württ)":null,Walkenried:null,"Wallau(Lahn)":null,"Walldorf(Hess)":null,"Walldorf(Werra)":null,"Walldürn":null,Wallersdorf:null,Wallertheim:null,Walleshausen:null,"Wallhausen(Helme)":null,"Wallhausen(Württ)":null,"Wallwitz(Saalkr)":null,Walpertskirchen:null,Walporzheim:null,Walschleben:null,Walsleben:null,Walsrode:null,Waltershausen:null,"Waltershausen Schnepfenthal":null,"Waltershausen Schnepfenthal":null,Wandersleben:null,Wandlitz:null,Wandlitzsee:null,"Wangen(Allgäu)":null,"Wangen(Unstrut)":null,"Wanne-Eickel Hbf":null,Wannweil:null,"Wansleben am See":null,"Warburg(Westf)":null,"Waren(Müritz)":null,Warendorf:null,"Warendorf-Einen-Müssingen":null,Warenshof:null,Warmbad:null,"Warnemünde":null,"Warnemünde Werft":null,Warngau:null,"Warnitz(Uckermark)":null,Warthausen:null,Warthausen:null,Wasbek:null,Wasenweiler:null,Wasseralfingen:null,"Wasserburg(Bodensee)":null,"Wasserburg(Günz)":null,"Wasserburg(Inn)Bf":null,Wasserliesch:null,Wasserthaleben:null,"Wassertrüdingen":null,"Wasserzell(b Eichstätt)":null,Wasungen:null,Wattenscheid:null,"Wattenscheid-Höntrop":null,Watzelsteg:null,"Watzenborn-Steinberg":null,Weckesheim:null,"Weddel(Braunschw)":null,"Wedel(Holst)":null,Weener:null,Weesenstein:null,Weetzen:null,Weeze:null,Wefensleben:null,Wega:null,Wegberg:null,Wegeleben:null,Wegenstedt:null,Wehdel:null,"Wehr(Mosel)":null,"Wehr-Brennet":null,Wehrden:null,"Wehretal-Reichensachsen":null,Wehrheim:null,Weibhausen:null,Weichering:null,"Weickersdorf(Sachs)":null,"Weickersgrüben":null,Weida:null,"Weida Altstadt":null,"Weida Mitte":null,"Weiden(Oberpf)":null,Weidenbach:null,Weidenberg:null,Weidenthal:null,Weiding:null,Weiherhammer:null,Weiherhof:null,Weikersheim:null,"Weil am Rhein":null,"Weil am Rhein Ost":null,"Weil am Rhein-Gartenstadt":null,"Weil am Rhein-Pfädlistraße":null,"Weil der Stadt":null,"Weil im Schönbuch Röte":null,"Weil im Schönbuch Troppel":null,"Weil im Schönbuch Untere Halde":null,"Weilbach(Unterallg)":null,"Weilbach(Unterfr)":null,Weilburg:null,"Weiler (Brohltal)":null,"Weiler(Rems)":null,Weilerswist:null,"Weilerswist-Derkum":null,"Weilheim(Oberbay)":null,"Weilheim(Württ)":null,Weilimdorf:null,Weimar:null,"Weimar Berkaer Bf":null,"Weimar West":null,"Weinböhla Hp":null,"Weingarten Berg":null,"Weingarten(Baden)":null,"Weinheim(Bergstr)":null,"Weinheim-Lützelsachsen":null,Weinsberg:null,"Weinsberg West":null,"Weinsberg/Ellhofen Gewerbegebiet":null,Weischlitz:null,Weisen:null,Weisenbach:null,"Weisenheim(Sand)":null,Weiterstadt:null,Weixdorf:null,"Weixdorf Bad":null,Weizen:null,"Weizern-Hopferau":null,"Weißandt-Gölzau":null,"Weißenau":null,"Weißenburg(Bay)":null,"Weißenfels":null,"Weißenfels West":null,"Weißenhorn":null,"Weißenhorn-Eschach":null,"Weißenohe":null,"Weißenthurm":null,"Weißer See":null,"Weißes Roß":null,"Weißwasser(Oberlausitz)":null,"Welgesheim-Zotzenheim":null,Welkers:null,"Wellen(Magdeburg)":null,"Wellen(Mosel)":null,Wellendorf:null,Wellmitz:null,"Welschen Ennest":null,"Welschingen-Neuhausen":null,Welver:null,"Wemmetsweiler Rathaus":null,"Wendisch Evern":null,"Wendisch-Rietz":null,"Wendlingen(Neckar)":null,Wennedach:null,"Wennigsen(Deister)":null,Wensickendorf:null,Werbig:null,Werbig:null,Werbig:null,Werdau:null,"Werdau Nord":null,"Werder(Havel)":null,Werdohl:null,Werdorf:null,Werl:null,"Wernau(Neckar)":null,Wernberg:null,"Werne a d Lippe":null,Werneuchen:null,Wernfeld:null,Wernigerode:null,"Wernigerode Elmowerk":null,"Wernigerode Hochschule Harz":null,"Wernigerode Westerntor":null,"Wernigerode-Hasserode":null,Wernshausen:null,"Wertach-Haslach":null,Wertheim:null,"Wertheim-Bestenheid":null,Werther:null,Wesel:null,"Wesel Feldmark":null,Wesenberg:null,Wesselburen:null,Wesseln:null,Westbarthausen:null,Westbevern:null,Westendorf:null,Westerburg:null,Westerham:null,Westerhausen:null,"Westerland(Sylt)":null,"Westerland(Sylt), Sylt Shuttle":null,"Westerstede-Ocholt":null,Westerstetten:null,"Westewitz-Hochweitzschen":null,Westhausen:null,"Westheim(Schwab)":null,"Westheim(Westf)":null,"Westheim-Langendorf":null,"Westönnen":null,"Wetter(Hessen)":null,"Wetter(Ruhr)":null,Wetterzeube:null,Wetzlar:null,"Weßling(Oberbay)":null,"Wickede(Ruhr)":null,Wicklesgreuth:null,Wickrath:null,Wiebelskirchen:null,Wiemersdorf:null,Wieren:null,"Wiesa(Erzgeb)":null,"Wiesau(Oberpf)":null,"Wiesbaden Hbf":null,"Wiesbaden Ost":null,"Wiesbaden-Biebrich":null,"Wiesbaden-Erbenheim":null,"Wiesbaden-Igstadt":null,"Wiesbaden-Schierstein":null,Wiesenau:null,"Wiesenburg(Mark)":null,"Wiesenburg(Sachs)":null,Wiesenfeld:null,"Wiesenfeld(b Coburg)":null,Wiesental:null,Wiesenthau:null,Wieslensdorf:null,"Wiesloch-Walldorf":null,"Wiesmühl(Alz)":null,Wiesthal:null,"Wilburgstetten Bf":null,"Wilchingen-Hallau":null,Wildau:null,"Wildberg(Württ)":null,"Wildeck-Bosserode":null,"Wildeck-Hönebach":null,"Wildeck-Obersuhl":null,Wildeshausen:null,"Wilferdingen-Singen":null,Wilgartswiesen:null,Wilhelmsdorf:null,"Wilhelmshaven Hbf":null,Wilhelmshorst:null,"Wilhelmshütte(Lahn)":null,Wilhermsdorf:null,"Wilhermsdorf Mitte":null,Wilischthal:null,"Wilkau-Haßlau":null,Willebadessen:null,Willingen:null,Willmenrod:null,Willmering:null,Willsbach:null,"Wilmersdorf(Angerm)":null,Wilsenroth:null,Wilster:null,Wilthen:null,"Wiltingen(Saar)":null,Wincheringen:null,Windelsbleiche:null,"Winden(Pfalz)":null,Windischeschenbach:null,Windsbach:null,Wingerode:null,Wingst:null,Winkelhaid:null,Winnenden:null,"Winningen(Mosel)":null,Winninghausen:null,Winnweiler:null,"Winsen(Luhe)":null,"Winterbach(b Schorndorf)":null,"Winterberg(Westf)":null,Winterhausen:null,Wintermoor:null,Wipperdorf:null,Wirges:null,Wirtheim:null,Wismar:null,"Wissen(Sieg)":null,Wissingen:null,"Wittbräucke":null,"Witten Hbf":null,"Witten-Annen Nord":null,Wittenberge:null,Wittenhagen:null,"Wittgensdorf Mitte":null,"Wittgensdorf ob Bf":null,Wittighausen:null,Wittingen:null,"Wittlich Hbf":null,Wittlingen:null,Wittmund:null,"Wittstock(Dosse)":null,"Witzenhausen Nord":null,Witzighausen:null,Witzschdorf:null,Witzwort:null,Woffleben:null,Wohltorf:null,Wolfach:null,Wolfegg:null,"Wolfen(Bitterfeld)":null,"Wolfenbüttel":null,"Wolfenbüttel":null,Wolferode:null,"Wolfgang(Kr Hanau)":null,Wolfhagen:null,Wolfratshausen:null,"Wolfsburg Hbf":null,"Wolfsgefärth":null,"Wolfsmünster":null,Wolfstein:null,Wolgast:null,"Wolgast Hafen":null,"Wolgaster Fähre":null,Wolkenstein:null,Wolkramshausen:null,"Wollbach(Baden)":null,Wolmirstedt:null,"Wolterdingen(Han)":null,"Woltersdorf/Nuthe-Urstromtal":null,Woltwiesche:null,"Worms Hbf":null,Worpswede:null,Wremen:null,Wriezen:null,Wrist:null,"Wulfen(Anh)":null,"Wulfen(Westf)":null,Wulften:null,Wullenstetten:null,"Wunsiedel-Holenbrunn":null,Wunstorf:null,"Wuppertal Hbf":null,"Wuppertal-Barmen":null,"Wuppertal-Langerfeld":null,"Wuppertal-Oberbarmen":null,"Wuppertal-Ronsdorf":null,"Wuppertal-Sonnborn":null,"Wuppertal-Steinbeck":null,"Wuppertal-Unterbarmen":null,"Wuppertal-Vohwinkel":null,"Wuppertal-Zoologischer Garten":null,Wurlitz:null,"Wurmlingen Mitte":null,"Wurmlingen Nord":null,"Wurzbach(Thür)":null,Wurzen:null,"Wusterhausen(Dosse) NE":null,Wustermark:null,Wusterwitz:null,"Wustrau-Radensleben":null,Wustweiler:null,Wutha:null,Wutike:null,"Wutöschingen":null,Wyhlen:null,"Wächterhof":null,"Wächtersbach":null,"Wölfershausen":null,"Wölfersheim-Södel":null,"Wörlitz":null,"Wörnitzstein":null,"Wörrstadt":null,"Wörsdorf":null,"Wörth(Isar)":null,"Wörth(Main)":null,"Wörth(Rhein)":null,"Wörth(Rhein) Alte Bahnmeisterei":null,"Wörth(Rhein) Badallee":null,"Wörth(Rhein) Badepark":null,"Wörth(Rhein) Bienwaldhalle":null,"Wörth(Rhein) Bürgerpark":null,"Wörth(Rhein) Mozartstraße":null,"Wörth(Rhein) Rathaus":null,"Wörth(Rhein) Zügelstraße":null,"Wössingen":null,"Wössingen Ost":null,"Wülfrath-Aprath":null,"Wülknitz":null,"Wünschendorf":null,"Wünschendorf Nord":null,"Wünsdorf-Waldstadt":null,"Würgendorf":null,"Würgendorf (Ort)":null,"Würzbach(Saar)":null,"Würzburg Hbf":null,"Würzburg Süd":null,"Würzburg-Zell":null,"Wüstenbrand":null,"Wüstenfelde":null,"Wüstenselbitz":null,"Wüsting":null,Xanten:null,Zabeltitz:null,Zachun:null,Zahna:null,Zaisenhausen:null,Zapfendorf:null,Zarrendorf:null,Zeesen:null,"Zehdenick(Mark)":null,"Zehdenick-Neuhof":null,Zeil:null,Zeithain:null,Zeitz:null,"Zell(Harmersbach)":null,"Zell(Wiesental)":null,"Zell-Romrod":null,"Zella-Mehlis":null,"Zella-Mehlis West":null,Zellendorf:null,Zellerthal:null,Zempin:null,Zennern:null,"Zepernick(Bernau)":null,Zeppelinheim:null,"Zerbst/Anhalt":null,Zerkall:null,Zernsdorf:null,Zerrenthin:null,"Zeulenroda unt Bf":null,"Zeutern Bf":null,"Zeutern Ost":null,"Zeutern Sportplatz":null,Zeuthen:null,Zeutsch:null,Zielitz:null,"Zielitz Ort":null,Zierenberg:null,"Zierenberg-Rosental":null,Zieverich:null,Zillendorf:null,Ziltendorf:null,"Zimmern(Main-Tauber)":null,"Zimmern(b Seckach)":null,Zimmersrode:null,Zinnowitz:null,Zirndorf:null,"Zirndorf Kneippallee":null,"Zirtow-Leussow":null,Zittau:null,"Zittau Hp":null,"Zittau Süd":null,"Zittau Vorstadt":null,Zoblitz:null,"Zollhaus(Villingen-Schwenningen)":null,"Zollhaus-Petersthal":null,Zorneding:null,Zossen:null,Zotzenbach:null,Zschaitz:null,Zscherben:null,Zschopau:null,"Zschopau Ost":null,Zschortau:null,Zusenhofen:null,Zuzenhausen:null,"Zweibrücken Hbf":null,"Zwenkau-Großdalzig":null,"Zwickau Stadthalle":null,"Zwickau Zentrum":null,"Zwickau(Sachs)Hbf":null,"Zwickau-Pölbitz":null,"Zwickau-Schedewitz":null,"Zwiesel(Bay)":null,Zwieselau:null,"Zwingenberg(Baden)":null,"Zwingenberg(Bergstr)":null,Zwota:null,"Zwota-Zechenbach":null,Zwotental:null,"Zwönitz":null,"Zöberitz":null,"Zörnigall":null,"Zühlsdorf":null,"Zülpich":null,"Züssow":null,"Züttlingen":null,"Äpfingen":null,"Öhringen Hbf":null,"Öhringen West":null,"Öhringen-Cappel":null,"Ölbronn-Dürrn":null,"Ötigheim":null,"Ötisheim":null,"Übach-Palenberg":null,"Überlingen":null,"Überlingen Therme":null,"Überlingen-Nußdorf":null,"Übersee":null,"Ückeritz":null,"Üdingen":null,"Ürzig(DB)":null,Pogeez:null,Klitten:null,Lohsa:null,"Mücka":null,Niesky:null,Uhyst:null,"Chemnitz Küchwald":null,Muldenberg:null,"Wengern Ost":null,Wallenrod:null,Alfhausen:null,Wangerooge:null,Barby:null,"Groß Behnitz":null,Haynsburg:null,Webau:null,Niederarnbach:null,"Leutershausen-Wiedersbach":null,Auersmacher:null,Brebach:null,"Hanweiler-Bad Rilchingen":null,Kleinblittersdorf:null,"Roßberg":null,"Pöllwitz":null,Kargow:null,Kleeth:null,"Rostock Seehafen Nord":null,"Hoyerswerda-Neustadt":null,Petershain:null,"Oberneuschönberg":null,Scheibenberg:null,"Thoßfell":null,Vohren:null,"Jütrichau":null,"Brandstätt":null,"Hildbrandsgrün":null,Otzing:null,Runding:null,"Schopfheim-Schlattholz":null,"Bübingen":null,Brefeld:null,"Güdingen":null,"Kaiserslautern-Hohenecken":null,Traun:null,"Flensburg-Weiche":null,"Ahrensfelde (S)":null,"Bernau (S)":null,"Berlin Charlottenburg (S)":null,"Erkner (S)":null,"Berlin Friedrichstraße (S)":null,"Potsdam Griebnitzsee (S)":null,"Hennigsdorf (S)":null,"Berlin-Hohenschönhausen (S)":null,"Berlin-Karlshorst (S)":null,"Berlin-Lichtenberg (S)":null,"Oranienburg (S)":null,"Berlin Ostbahnhof (S)":null,"Potsdam Hbf (S)":null,"Berlin-Spandau (S)":null,Colditz:null,"Eich(Sachs)":null,"Marienberg(Sachs)":null,Penig:null,"Plauen(V) unt Bf":null,"Rochlitz(Sachs)":null,"Straßgräbchen-Bernsdorf":null,Teichwolframsdorf:null,"Waldenburg(Sachs)":null,"Münster-Mecklenbeck":null,"Allendorf(Eder) Bf":null,"Bergheim-Giflitz":null,"Bürgerhaus, Hessisch Lichtenau":null,"Bahnhof Niederzwehren, Kassel":null,"ZOB, Duderstadt":null,"Dambeck(Altm)":null,Jerichow:null,Nedlitz:null,"Sandersdorf(Bitterf)":null,Siedenlangenbeck:null,"Sieversdorf(Neust/D)":null,Ziesar:null,Ottobeuren:null,"Rohrdorf(Oberbay)":null,"München-Süd":null,"Merzig(Saar) Ost":null,"Vaihingen(Enz)Nord":null,"Berka(Wipper)":null,Vacha:null,"Friedrichswalde(bei Eberswalde)":null,Ganzlin:null,"Klockow(b Waren/Müritz)":null,Milmersdorf:null,"Malliß":null,Chop:null,"Brest Central":null,"Baranovichi Centralnye":null,"Minsk-Passajirskii":null,"Orscha Central":null,"Barchel, Oerel":null,"Hamburg Elbbrücken":null,"Essel, Kutenholz":null,Fredenbeck:null,"Hagen, Stade":null,"Hemsen(b Soltau)":null,"Kiel-Ellerbek":null,Wagersrott:null,"Blankenfelde (S)":null,"Berlin-Mahlsdorf":null,"Augustusburg Bergstation":null,"Buschmühle":null,"Einsiedel Brauerei":null,"Schmiedeberg-Naundorf":null,Ulberndorf:null,"Bornholte(b Verl)":null,"Emmerich-Elten":null,"Höste":null,Oese:null,Ballersbach:null,Eisemroth:null,Inheiden:null,"Altmarkt/Regierungspräsidium, Kassel":null,"Am Stern, Kassel":null,"Am Kupferhammer, Kassel":null,"Forstfeldstraße, Kassel":null,"Kirchgasse, Kassel":null,"Sandershäuser Straße, Kassel":null,"Lindenberg, Kassel":null,"Hinter dem Fasanenhof, Kassel":null,"Leipziger Platz, Kassel":null,"Leipziger Straße, Kassel":null,"KVG-Betriebshof, Kassel":null,"Friedrichsplatz, Kassel":null,"Hegelsbergstraße, Kassel":null,"Hauptfriedhof, Kassel":null,"Holländischer Platz/Universität, Kassel":null,"Holländische Straße, Kassel":null,"Königsplatz, Kassel":null,"Lutherplatz, Kassel":null,"Halitplatz, Kassel":null,"Auestadion, Kassel":null,"Heinrich-Heine-Straße, Kassel":null,"Am Weinberg, Kassel":null,"Platz der Deutschen Einheit, Kassel":null,"Rathaus, Kassel":null,"Rathaus/Fünffensterstraße, Kassel":null,"Scheidemannplatz, Kassel":null,"Unterneustädter Kirchplatz, Kassel":null,"Wilhelmsstraße/Stadtmuseum, Kassel":null,"Wiener Straße, Kassel":null,"Liebenau(Bz Kassel)":null,"Papierfabrik, Kaufungen":null,Arbste:null,"Dünsen DHE":null,"Groß Ippener DHE":null,Hademstorf:null,Harlesiel:null,"Delmenhorst Hasporter Damm":null,Jerxheim:null,"Nordhorn-Blanke":null,"Neuenhaus Süd":null,"Nammen-Bad":null,"Stelle DHE":null,Schwindebeck:null,Watenstedt:null,"Halle(S) Heidebf":null,"Leipzig Essener Straße":null,Meisdorf:null,Feldolling:null,Finsterwald:null,"Hammersbach Zugspitzbahn, Grainau":null,Kaltenberg:null,"Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc":null,"Margertshausen Bf":null,"Pfraundorf(Inn)":null,"Rosenheim Aicherpark":null,"Riffelriß, Grainau":null,"Reichertshofen(Schwab) Bf":null,Zainhammer:null,"Erbprinz/Schloss, Ettlingen":null,"Albgaubad, Ettlingen":null,"Neuwiesenreben, Ettlingen":null,"Spinnerei, Ettlingen":null,"Wasen, Ettlingen":null,"Bahnhof, Gönnheim":null,"Ittersbach Industrie, Karlsbad":null,"Augartenstraße, Karlsruhe":null,"Poststraße, Karlsruhe":null,"Kronenplatz (Kaiserstraße), Karlsruhe":null,"Gottesauer Platz/BGV, Karlsruhe":null,"Dammerstock, Karlsruhe":null,"Durlach Hubstraße, Karlsruhe":null,"Durlach Untermühlstraße, Karlsruhe":null,"Weinweg, Karlsruhe":null,"Knielingen Eggensteiner Straße, Karlsruhe":null,"Händelstraße, Karlsruhe":null,"Philippstraße, Karlsruhe":null,"Knielingen Herweghstraße, Karlsruhe":null,"Knielingen Siemens, Karlsruhe":null,"Ettlinger Tor, Karlsruhe":null,"Herrenstraße, Karlsruhe":null,"Kongresszentrum, Karlsruhe":null,"Schillerstraße, Karlsruhe":null,"Mühlburg West, Karlsruhe":null,"Yorckstraße, Karlsruhe":null,"Neureut Bärenweg, Karlsruhe":null,"Europapl./Postgalerie (Karl), Karlsruhe":null,"Europapl./PostGalerie (Karlstr.), Karlsruhe":null,"Europapl./PostGalerie (Kaiserstr), Karlsruhe":null,"Lameyplatz, Karlsruhe":null,"Starckstraße, Karlsruhe":null,"Kullenmühle, Bad Herrenalb":null,"Langensteinbach St. Barbara, Karlsbad":null,"Ludwigshafen(Rhein) Oppau":null,"Reichenbach Kurpark, Waldbronn":null,"Rüppurr Battstraße, Karlsruhe":null,"Rüppurr Ostendorfplatz, Karlsruhe":null,"Rüppurr Tulpenstraße, Karlsruhe":null,"Schloss Rüppurr, Karlsruhe":null,"Heilbronn Karlstor":null,"Großbodungen":null,Gebersdorf:null,"Stolberg(Harz)":null,Tiefenort:null,Zopten:null,"Alt Schwerin":null,Bantin:null,Bobzin:null,"Feldberg(Meckl)":null,Gildenhall:null,"Götschendorf":null,"Malk Göhren":null,"Seebad Lubmin":null,"Ringenwalde(Templin)":null,"Templin-Ahrensdorf":null,"t Harde":null,Aalter:null,Aarau:null,"Aarburg-Oftringen":null,Achiet:null,"Affoltern am Albis":null,Agde:null,Aigle:null,"Aime-la-Plagne":null,Airole:null,Airolo:null,"Aix-en-Provence TGV":null,"Aix-les-Bains-le-Revard":null,Alassio:null,"Alba Iulia":null,"Albate-Camerlata":null,Albenga:null,Albertville:null,"Alençon":null,Ales:null,"Alken(B)":null,Alkmaar:null,"Allerheiligenhöfe":null,Almelo:null,"Almere Oostvaarders":null,"Alphen aan den Rijn":null,"Altdorf(CH)":null,"Altendorf(CH)":null,"Altenmarkt im Pongau":null,Altnau:null,"Altstätten SG":null,Amberieu:null,"Amersfoort Vathorst":null,Amersfoort:null,"Amsterdam Sloterdijk":null,"Amsterdam Bijlmer ArenA":null,"Amsterdam Centraal":null,"Amsterdam Zuid":null,"Amsterdam Amstel":null,"Amstetten NÖ":null,Andelfingen:null,Andermatt:null,Andorf:null,Angleur:null,Angouleme:null,"Ans(B)":null,Antibes:null,"Antwerpen Centraal":null,"Antwerpen-Zuid":null,"Apach(Moselle)":null,Apeldoorn:null,Appingedam:null,Arad:null,Arbon:null,"Arbon (See)":null,Arezzo:null,"Argeles-sur-Mer":null,Arles:null,Arlon:null,"Arnhem Centraal":null,Arnoldstein:null,Arosa:null,"Arras(F)":null,"Artenay(Loiret)":null,"Arth-Goldau":null,Arvant:null,"As(CZ)":null,"Ashford(Kent)":null,"Ashford(Kent) Int.":null,Asse:null,"Attnang-Puchheim":null,"Au SG":null,"Au ZH":null,"Aulnoye Aymeries":null,"Auneau(Dourdan)":null,Auvelais:null,"Avesnes-sur-Helpe":null,"Avignon Centre":null,"Avignon TGV":null,Babylon:null,"Bäch":null,"Bad Aussee":null,"Bad Blumau":null,"Bad Gastein":null,"Bad Hofgastein":null,"Bad Ischl":null,"Bad Ragaz":null,"Bad Schallerbach-Wallern":null,"Bad Vigaun":null,"Bad Zurzach":null,"Baden(CH)":null,Baisieux:null,Balerna:null,"Banova Jaruga":null,Bantzenheim:null,"Banyuls-sur-Mer":null,"Barcelona Sants":null,Barendrecht:null,"Bar-le-Duc":null,"Bascharage-Sanem":null,"Basel SBB":null,"Basel Dreispitz":null,"Basel St Johann":null,Bassersdorf:null,Battipaglia:null,Baunhoej:null,Bayonne:null,Beernem:null,Bekescsaba:null,"Bela pod Bezdezem":null,"Belfort Ville":null,"Bellegarde(Ain)":null,"Belleville Meurthe et Moselle":null,Bellinzona:null,Belp:null,"Belval Lycée":null,"Belval-Rédange":null,"Belval-Université":null,"Belvaux-Soleuvre":null,"Benesov n. Ploucnici":null,Benestroff:null,"Benfeld(Selestat)":null,Bening:null,"Antwerpen-Berchem":null,"Berchem(LUX)":null,"Berg(CH)":null,"Bergen op Zoom":null,"Bergues(Coudek)":null,"Bergün/Bravuogn":null,"Berlingen(CH)":null,"Berlingen URh":null,Bern:null,Bernay:null,Beroun:null,Berthelming:null,"Bertrange-Strassen":null,Bertrix:null,"Besançon-Mouillère":null,"Besançon-Viotte":null,"Bettembourg(fr)":null,Bettembourg:null,Bettwiesen:null,"Betzdorf(LUX)":null,Bevera:null,Beverwijk:null,Bex:null,Beziers:null,Biarritz:null,Biasca:null,"Biberist RBS":null,"Biberist Ost":null,"Bichlbach Almkopfbahn":null,"Bichlbach-Berwang":null,"Biel/Bienne":null,"Bierset-Awans":null,Bilina:null,"Billum st":null,Bilten:null,"Bily Kostel nad Nisou":null,"Birmensdorf ZH":null,Bischofshofen:null,"Blainville-Damelevieres":null,Blaj:null,"Le Blanc-Mesnil":null,"Blausee-Mitholz":null,Blerick:null,Blindenmarkt:null,Bludenz:null,"Böckstein":null,Bodegraven:null,"Bodio TI":null,"Boen(F)":null,"Böheimkirchen":null,Bohumin:null,"Bollwiller(Lutterb)":null,"Bologna Centrale":null,"Bolzano/Bozen":null,"Boondael/Boondaal":null,"Bordeaux-St-Jean":null,Bordighera:null,"Borgo S. Dalmazzo":null,"Borne(NL)":null,"Borup st":null,Bottighofen:null,"Boulevarden st":null,"Bourg-en-Bresse":null,Bourges:null,"Bourg-St.Maurice":null,Bouzonville:null,"Bramming st":null,Brasov:null,"Bratislava hl.st.":null,"Bratislava-Petrzalka":null,"Braunau/Inn":null,Breclav:null,"Breclav(Gr)":null,Breda:null,"Bredebro st":null,Bregenz:null,"Bregenz Hafen":null,"Breil-sur-Roya":null,"Brennero/Brenner":null,Brescia:null,"Bressanone/Brixen":null,Bressoux:null,"Brest(F)":null,"Breziny u Decina":null,Brig:null,"Brixen im Thale":null,Brixlegg:null,"Broens st":null,Bronschhofen:null,"Bruck/Mur":null,"Bruck/Leitha":null,"Bruck-Fusch":null,"Brugg AG":null,Brugge:null,Brumath:null,"Brunico/Bruneck":null,"Brunnen(CH)":null,"Bruxelles-Midi Eurostar":null,"Bruxelles-Central":null,"Bruxelles-Luxembourg":null,"Bruxelles Midi":null,"Brussels Airport - Zaventem":null,"Bruxelles-Nord":null,"Buchs SG":null,"Bucuresti Nord Gara A":null,"Budapest-Keleti":null,"Budapest-Nyugati":null,Buitenpost:null,"Bülach":null,"Bully-Grenay":null,"Burgfried b.Gnas":null,"Bürglen":null,"Burgos Rosa de Lima":null,Busigny:null,Bussnang:null,"Busto Arsizio":null,"Bydgoszcz Glowna":null,"Colle Isarco/Gossensass":null,Cadenazzo:null,"Calais Ville":null,"Calais-Fréthun":null,"Caldes de Malavella":null,"Campo di Trens/Freienfeld":null,Cannes:null,"Cannes-la-Bocca":null,Capellen:null,"Capolago-Riva S. Vitale":null,"Carbonne(Boussens)":null,Carcassonne:null,Carimate:null,"Carnoules(Toulon)":null,"Casteldarne/Ehrenburg":null,Castelnaudary:null,"Castione-Arbedo":null,"Cavaillon(Avignon)":null,Centallo:null,"Cents-Hamm":null,"Cerbère":null,"Cernovice u Chomutova":null,"Ceska Kamenice":null,"Ceska Kubice":null,"Ceska Lipa hl.n.":null,"Ceska Lipa strelnice":null,"Ceska Trebova":null,"Ceske Budejovice":null,"Ceske Velenice":null,"Chalons en Champagne":null,"Chalon sur Saône":null,"Chambery-Challes-E":null,Champigneulles:null,"Charleroi Sud":null,"Château-Arnoux-St-Auban":null,"Château du Loir":null,"Châteauroux":null,"Chateau-Thierry":null,"Châtelet":null,"Chauny(Tergnier)":null,Cheb:null,"Cheb-Skalka":null,"Chelles Gournay":null,"Chenay Gagny":null,Chenee:null,Cherbourg:null,Chiasso:null,"Chiusa/Klausen":null,"Chiusi-Chianciano Terme":null,Chomutov:null,"Chomutov mesto":null,Chotyne:null,Chrastava:null,"Chrastava-Andelska Hora":null,Chribska:null,Chur:null,Cintegabelle:null,"Clermont-Ferrand":null,Clerval:null,Clervaux:null,Coevorden:null,Collioure:null,Colmar:null,"Combs la Ville Quincy":null,"Como S. Giovanni":null,Compiegne:null,Conegliano:null,"Conflans-Jarny":null,"Corbehem(Douai)":null,"Corbeil Essonnes":null,Cornaux:null,Cosne:null,"Cossonay-Penthalaz":null,Coulommiers:null,"Courcelles-sur-Nied":null,Coutras:null,Creil:null,"Culmont-Chalindrey":null,Culoz:null,Cuneo:null,Curtici:null,"Czechowice-Dziedzice":null,Czerwiensk:null,Dachsen:null,Dagmersellen:null,"Däniken":null,"Davos Dorf":null,"Davos Platz":null,Dax:null,"Enschede De Eschmarke":null,Debrecen:null,"Decin hl.n.":null,"Decin vychod":null,"Decin-Certova voda":null,"Decin-Priper":null,"Decin-Prostredni Zleb":null,Delden:null,"Delémont":null,Delft:null,Delfzijl:null,"Den Haag Centraal":null,"Den Haag HS":null,"Den Helder":null,Denderleeuw:null,Desenice:null,"Desenzano del Garda/Sirmione":null,Desio:null,Dettwiller:null,Deva:null,Deventer:null,"Devinska Nova Ves":null,"Diano Marina":null,Diemeringen:null,Diepenbeek:null,Diessenhofen:null,"Diessenhofen URh":null,Dietlikon:null,Dieulouard:null,Differdange:null,"Dijon Ville":null,"Dijon Porte Neuve":null,"Dippach-Reckange":null,"Dobbiaco/Toblach":null,Dobova:null,"Dobova(Gr)":null,"Doestrup(Soenderjylland) st":null,Doksy:null,"Dole Ville":null,"Dolhain-Gileppe":null,"Dolni Habartice":null,"Dolni Podluzi":null,"Dolni Poustevna":null,"Dolni Zleb":null,"Dolni Zleb zast.":null,Domazlice:null,Domina:null,Dommeldange:null,Domodossola:null,Dordrecht:null,Dorfgastein:null,Dornbirn:null,"Dornbirn Schoren":null,"Dottikon-Dintikon":null,Dourges:null,Drauffelt:null,Dronten:null,Duchcov:null,"Dugo Selo":null,Duivendrecht:null,Dunkerque:null,"Dyreby st":null,"Ebbsfleet International Eurostar":null,"Eben im Pongau":null,Ebenfurth:null,Ebersheim:null,"Ede(B)":null,"Ede Centrum":null,"Ede-Wageningen":null,Effretikon:null,Eglisau:null,Egnach:null,"Ehrwald Zugspitzbahn":null,Eindhoven:null,Einsiedeln:null,Elne:null,Elsbethen:null,"Emmenbrücke":null,Engis:null,Enns:null,Enschede:null,Epernay:null,"Epierre-St Leger":null,Epinal:null,Erlen:null,Ermatingen:null,"Ermatingen URh":null,Erquelinnes:null,Erstein:null,Erstfeld:null,"Esbjerg st":null,"Eschenau/Salzach":null,"Esch-sur-Alzette":null,"Essen(B)":null,Etampes:null,Ettelbruck:null,Etzwilen:null,Eupen:null,"Evreux Normandie":null,"Eygelshoven Markt":null,"Faak am See":null,Faido:null,Faulquemont:null,"Fegersheim Lipsheim":null,Fehraltorf:null,Feldkirch:null,"Feldkirchen in Kärnten":null,Felixdorf:null,"Budapest-Ferencváros":null,Ferrara:null,Fieberbrunn:null,Figueres:null,Filisur:null,"Finale Ligure Marina":null,"Firenze S.M.N.":null,Flamatt:null,Flassa:null,Flaurling:null,Flers:null,"Wien Floridsdorf":null,"Flüelen":null,Flums:null,"Föderlach":null,Fontaine:null,"Fontainebleau-Avon":null,"Fontan Saorge":null,"Forbach(F)":null,"Forest Midi/Vorst Zuid":null,"Fortezza/Franzensfeste":null,Fossano:null,Fourchambault:null,Fraipont:null,Frankenmarkt:null,"Frantiskovy Lazne Aquaforum":null,"Frantiskovy Lazne":null,Frastanz:null,Frauenfeld:null,"Fredericia st":null,"Freienbach SBB":null,"Frenkendorf-Füllinsdorf":null,"Fribourg/Freiburg":null,"Friesach in Kärnten":null,"Frisvadvej st":null,"Fritzens-Wattens":null,Frontenex:null,Frouard:null,Frutigen:null,"Fürnitz":null,Futuroscope:null,Gagny:null,"Gaillon Aubevoye":null,Gallarate:null,"Gampel-Steg":null,"Gandrange-Amneville":null,Gannat:null,"Gänserndorf":null,Gardanne:null,"Gdansk Glowny":null,"Gdansk Oliwa":null,"Gdansk Wrzeszcz":null,"Gdynia Glowna":null,Geinberg:null,"Geleen-Lutterade":null,Gelterkinden:null,"Gemona del Friuli":null,"Genève":null,"Genève-Aéroport":null,Genk:null,"Genova Piazza Principe":null,Gentbrugge:null,"Gent-Dampoort":null,"Gent St Pieters":null,Gerlafingen:null,"Gerling im Pinzgau":null,Girona:null,"Gevrey-Chambertin":null,"Gießenbach in Tirol":null,"Gisikon-Root":null,"Gisors Embranchement":null,Giubiasco:null,"Gjesing st":null,Glanerbrug:null,Glattbrugg:null,Glattfelden:null,Gloggnitz:null,"Gmünd NÖ":null,Gniezno:null,"Goebelsmühle":null,"Golling-Abtenau":null,Goppenstein:null,"Gorzow Wlkp.":null,"Göschenen":null,"Gossau SG":null,"Gottlieben (Schifflände)":null,"Götzendorf/Leitha":null,"Götzis":null,Gouda:null,Gouvy:null,Graffenstaden:null,Gramatneusiedl:null,Granollers:null,"Gratwein-Gratkorn":null,"Graz Hbf":null,"Graz Ostbahnhof-Messe":null,"Gredstedbro st":null,"Grenchen Nord":null,Grenoble:null,"Gresy-sur-Isere":null,"Gretz-Armainvilliers":null,"Gries am Brenner":null,"Gries im Pinzgau":null,"Grieskirchen-Gallspach":null,"Gröbming":null,Groenendaal:null,Groningen:null,Schwechat:null,"Guldager st":null,"Guntramsdorf Kaiserau":null,"Gurten OÖ":null,"Güttingen":null,"Györ":null,Haaltert:null,Haarlem:null,"Haarlem Spaarnwoude":null,"Häggenschwil-Winden":null,"Hagenau im Innkreis":null,Hagondange:null,Haguenau:null,Haiming:null,"Hall in Tirol":null,Hallein:null,"Hallwang-Elixhausen":null,"Hard-Fussach":null,Hasselt:null,"Hatting in Tirol":null,Hauptwil:null,"Haus im Ennstal":null,Hayange:null,Hazebrouck:null,Heerbrugg:null,Heerenveen:null,Heerlen:null,"Hegyeshalom(Gr)":null,Hegyeshalom:null,"Heiterwang-Plansee":null,Helmond:null,Hendaye:null,Hendschiken:null,Hengelo:null,"Henin-Beaumont":null,"Henne st":null,Herentals:null,Hergenrath:null,"Wien Hernals":null,Herny:null,"Herrlisheim près Colmar":null,Herstal:null,Herzele:null,Herzogenbuchsee:null,Herzogenburg:null,Hillegom:null,Hilversum:null,"Hilversum Sportpark":null,Hnevice:null,Hochfelden:null,Hochfilzen:null,Hochzirl:null,Hoeilaart:null,"Hoeje Taastrup st":null,Hohenau:null,Hohenems:null,"Hombourg-Haut":null,Hoofddorp:null,Hoogeveen:null,"Hoogezand-Sappemeer":null,"Hopfgarten im Brixental":null,"Hopfgarten im Brixental Berglift":null,Horgen:null,"Horn(Bodensee)":null,"Horn(Bodensee), SF":null,"Horni Blatna":null,"Horni Dvoriste":null,"Horni Kamenice":null,"Horni Podluzi":null,"Horni Poustevna":null,Horovice:null,"Hörsching":null,"Hradek nad Nisou":null,"Hranice na Morave":null,Hrebeny:null,"Hüntwangen-Wil":null,"Hüttau":null,"Huy(B)":null,"Hviding st":null,"Hyllerslev st":null,"Ilawa Glowna":null,Immensee:null,Imsterberg:null,"Imst-Pitztal":null,Ingelmunster:null,Ingwiller:null,"Innsbruck Hbf":null,"Innsbruck Hötting":null,"Innsbruck Westbahnhof":null,Inowroclaw:null,"Interlaken Ost":null,"Interlaken West":null,"Inzing/Inn":null,Irun:null,"Is-sur-Tille":null,"Iselle di Trasquera":null,"Iselle transito":null,"Ivanic Grad":null,"Jablonne v Podjestedi":null,"Janderup st":null,"Jankowa Zaganska":null,"Janovice nad Uhlavou":null,"Wien Jedlersdorf":null,Jedlova:null,"Jegum st":null,"Jelenia Gora":null,"Rochefort-Jemelle":null,Jenbach:null,"Jenbach Zillertalbahn":null,"Jesenice(SL)":null,Jestetten:null,Jeumont:null,"Jiretin pod Jedlovou":null,"Jirkov zast.":null,Joeuf:null,"Joigny(Lar.Migennes)":null,Judenburg:null,Juvisy:null,"Kadan-Prunerov":null,Kaiseraugst:null,Kalhausen:null,"Kalsdorf b.Graz":null,Kalwang:null,"Kampen Zuid":null,Kandersteg:null,Kapfenberg:null,"Karlovy Vary dolni n.":null,"Karlovy Vary":null,Katowice:null,Kautenbach:null,Kehlhof:null,Kelebia:null,"Kelenföld":null,"Kematen in Tirol":null,Kesswil:null,Kesteren:null,"Kilchberg(CH)":null,"Killwangen-Spreitenbach":null,Kindberg:null,"Kirchberg in Tirol":null,Kirchbichl:null,Kittsee:null,"Kitzbühel Hahnenkamm":null,"Kitzbühel":null,"Klagenfurt Hbf":null,Klatovy:null,"Kledering b.Wien":null,"Wien Kaiserebersdorf":null,Kleinbettingen:null,Kloten:null,Knittelfeld:null,"Koblenz(CH)":null,"Koebenhavn H":null,"Koebenhavns Lufthavn st":null,"Koege st":null,Kogenheim:null,Kolbnitz:null,"Kolding st":null,"Kolin(CZ)":null,Komarom:null,Konin:null,"Koog aan de Zaan":null,"Zaandijk Zaanse Schans":null,"Körmend":null,"Korsoer st":null,Kortenberg:null,Kortrijk:null,Kovarska:null,"Krakow Glowny":null,"Kralupy nad Vltavou":null,Kranebitten:null,Kranj:null,Kraslice:null,"Kraslice-Pod vlekem":null,"Kraslice predmesti":null,"Krasna Lipa":null,"Krasna Lipa mesto":null,"Krems an der Donau":null,"Kreuzlingen Bernrain":null,Kreuzlingen:null,"Kreuzlingen Hafen":null,"Kirchdorf/Krems":null,"Krimmeri-Meinau":null,Krimov:null,"Krommenie-Assendelft":null,Kropswolde:null,Krsko:null,"Krumpendorf/Wörthersee":null,"Krzewina Zgorzelecka":null,Kuchl:null,Kufstein:null,Kundl:null,Kunowice:null,"Küssnacht am Rigi":null,Kutina:null,Kutno:null,Kuty:null,Kytlice:null,"La Bastide-St-Laurent les Bains":null,"La Brigue(F)":null,"La Charité sur Loire":null,"La Plaine":null,"La Roche sur Yon":null,"La Souterraine":null,"Laa/Thaya":null,Lachen:null,"Lage Zwaluwe":null,"Lähn":null,LAigle:null,Lamadelaine:null,Lambach:null,"Lamone-Cadempino":null,"Landeck-Zams":null,Landen:null,Landgraaf:null,Landquart:null,Landry:null,Langdorp:null,"Langen am Arlberg":null,"Langenthal(CH)":null,Langkampfen:null,"Läufelfingen":null,"Laufen(CH)":null,"Laufenburg(CH)":null,Lausanne:null,"Lausanne-Flon":null,
-"Lausen(CH)":null,Lauterach:null,Lauterbourg:null,"Laveno Mombello":null,"Le Bourget":null,"Le Creusot Montceau Montchanin TGV":null,"Le Havre":null,"Le Mans":null,"Le Raincy Villemomble Montferm":null,Lebbeke:null,Leeuwarden:null,Legnica:null,Leibnitz:null,"Leiden Centraal":null,"Leiden Lammenschans":null,"Leithen b.Seefeld":null,"Lelystad Centrum":null,Lend:null,Lengwil:null,"Lens(F)":null,Lenzburg:null,Lenzing:null,"Leoben Hbf":null,Leogang:null,Leopoldsburg:null,Lermoos:null,Lerouville:null,"Les Arcs Draguignan":null,"Les-Aubrais-Orleans":null,"Lesce-Bled":null,Leudelange:null,Leuk:null,Leuven:null,Lezignan:null,Liberec:null,"Liège-Guillemins":null,"Lienz in Osttirol":null,Liers:null,Liestal:null,Liezen:null,"Lille Flandres":null,"Lille Europe":null,Limone:null,"Linz Hbf":null,"Linz/Donau Wegscheid":null,"Lipinki Luzyckie":null,"Lipova u Sluknova":null,Litija:null,"Litomerice mesto":null,"Livorno Centrale":null,Ljubljana:null,Locarno:null,"Lochau-Hörbranz":null,"Loeftgaard st":null,"Lököshaza":null,Longueau:null,Longwy:null,"Lons-Le-Saunier":null,"Loosdorf b.Melk":null,Lorraine:null,Lottstetten:null,Lourches:null,Lourdes:null,Lovosice:null,"Luban Sl.":null,Ludesch:null,Lugano:null,"Luh nad Svatavou":null,Luino:null,"Lumes Halte":null,"Lunde J st":null,"Lunderskov st":null,Lunel:null,"Lunéville":null,Lupfig:null,Lustenau:null,"Luterbach-Attisholz":null,Lutterbach:null,Lutzelbourg:null,Luxembourg:null,Luzern:null,"Lyon Part Dieu":null,Maastricht:null,Machnin:null,"Machnin hrad":null,"Mâcon Ville":null,"Mâcon-Loché TGV":null,Maienfeld:null,"Maishofen-Saalbach":null,"Mala Velen":null,Malbork:null,Malczyce:null,"Mallnitz-Obervellach":null,"Malmö Central":null,Mamer:null,"Mamer Lycée":null,"Mammern(Bodensee)":null,"Mammern URh":null,Manage:null,"Mannenbach-Salenstein":null,"Mannenbach URh":null,Manternach:null,Marchegg:null,"Marche-les-Dames":null,"Marchienne au Pont":null,Marchtrenk:null,Maribor:null,Markvartice:null,"Marle-sur-Serre":null,Marloie:null,"Marne la Vallée-Chessy":null,"Marseille-Blancarde":null,"Marseille-St-Charles":null,Martenshoek:null,Martigny:null,Martigues:null,"Märwil":null,"Matrei am Brenner":null,Maubeuge:null,"Mautern im Liesingtal":null,"Mayrhofen im Zillertal":null,Mechelen:null,Medias:null,Meiringen:null,Melk:null,Mels:null,Melun:null,Mendrisio:null,Menton:null,Menznau:null,"Merano/Meran":null,Merchtem:null,Merelbeke:null,"Mersch(LUX)":null,Mertert:null,"Merxheim(Colmar)":null,Raaba:null,"Metz Ville":null,"Meuse TGV":null,"Michelau(LUX)":null,Micheldorf:null,"Middelfart st":null,Miekinia:null,"Mikulasovice dol.n.":null,"Milano Centrale":null,"Milano Greco Pirelli":null,"Milano Porta Garibaldi":null,Milmort:null,Mimon:null,Mining:null,Miramas:null,"Mitterberghütten":null,"Mitterdorf-Veitsch":null,"Mixnitz Bärenschützklamm":null,"Mlada Boleslav hl.n.":null,"Mlyny(CZ)":null,Modane:null,"Möhlin":null,Mol:null,Mols:null,Mommenheim:null,"Monaco-Monte-Carlo":null,"Monguelfo-Casies/Welsberg-Gsies":null,Mons:null,"Montbéliard Ville":null,Montelimar:null,Monthey:null,"Montluçon Ville":null,Montmelian:null,"Montpellier Saint-Roch":null,Montreux:null,Monza:null,"Moosbierbaum-Heiligeneich":null,"Moret-Veneux-les-Sablons":null,Morges:null,Morhange:null,"Moskva Belorusskaja":null,Mosonmagyarovar:null,Most:null,"Mötz":null,Mouchard:null,"Moulins-sur-Allier":null,Mouscron:null,Moustier:null,Moutier:null,"Moutiers-Salins-Brides-les-Bains":null,"Moyeuvre-Grande":null,"Mühlehorn":null,"Mühlheim am Inn":null,"Mühldorf-Möllbrücke":null,Muizen:null,"Mulhouse-Dornach":null,"Mulhouse Ville":null,"Münchenbuchsee":null,Mundolsheim:null,Munkzwalm:null,Munsbach:null,"Münsingen(CH)":null,"Munster(Metzeral)":null,"Münster-Wiesing":null,"Münsterlingen-Scherzingen":null,Muolen:null,"Murg(CH)":null,"Mürzzuschlag":null,Musau:null,Mussidan:null,Muttenz:null,"Naestved st":null,"Nagymaros-Visegrad":null,Namur:null,"Nancois Tronville":null,"Nancy Ville":null,Nantes:null,Narbonne:null,Nebikon:null,Neerpelt:null,Nejdek:null,"Nejdek zastavka":null,"Nemours St Pierre":null,Nendeln:null,Nenzing:null,Nessonvaux:null,Nestedice:null,Nestemice:null,Nettingsdorf:null,"Neuchâtel":null,"Neufchateau(B)":null,"Neufchateau(F)":null,"Neuhausen(CH)":null,"Neukirch-Egnach":null,"Neumarkt-Kallham":null,"Neumarkt-Köstendorf":null,Neuratting:null,"Nice Ville":null,Niederbipp:null,Niederglatt:null,Niederkorn:null,"Nieuw Amsterdam":null,"Bad Nieuweschans":null,Nijmegen:null,"Nîmes":null,"Noerre Nebel st":null,"Noerreport st":null,Noertzange:null,"Nogent-le-Rotrou":null,"Noisy-le-Sec":null,"Notre-Dame-de-Briancon":null,"Nova Gradiska":null,"Nova Kapela":null,"Nova Role":null,"Nova Role zastavka":null,Novara:null,"Nove Hamry":null,"Nejdek-Sejfy":null,"Nove Zamky":null,Noveant:null,"Novi Beograd":null,"Novi Sad":null,Novska:null,"Novy Bor":null,Noyon:null,"Ny Ellebjerg st":null,"Nyborg st":null,Nyiregyhaza:null,"Nykoebing F st":null,"Nymburk hl.n.":null,Oberalm:null,Oberburg:null,Oberglatt:null,"Oberhofen im Inntal":null,Oberkorn:null,Obermodern:null,"Obernberg-Altheim":null,"Oberrieden(CH)":null,Oberriet:null,Obourg:null,"Odense st":null,Oermingen:null,"Oesterport st":null,Oetrange:null,"Oksboel st":null,Okucani:null,Oldenzaal:null,"Nejdek-Oldrichov":null,Olen:null,"Olivetta-S. Michele":null,Olovi:null,Olten:null,Ommen:null,Onville:null,Oostende:null,"Opole Glowne":null,Oppikon:null,Opwijk:null,"Orange(Avignon)":null,Orchies:null,"Orléans":null,Orvieto:null,Oss:null,Ostermundigen:null,"Ostrava hl.n.":null,"Ostrava-Svinov":null,Oswiecim:null,Othmarsingen:null,"Ötztal":null,"Outrup st":null,Overveen:null,"Ponte Gardena-Laion/Waidbruck-Lajen":null,"Padborg st":null,Padova:null,"Pagny-sur-Moselle":null,Pantin:null,"Pardubice hl.n.":null,"Paris Montparnasse":null,"Paris Nord":null,"Paris Austerlitz":null,"Paris Est":null,"Paris Gare de Lyon":null,"Paris St Lazare":null,Parndorf:null,"Paternion-Feistritz":null,Patsch:null,Pau:null,Pavia:null,"Peggau-Deutschfeistritz":null,Peltre:null,Pepinster:null,Perigueux:null,Pernink:null,Perpignan:null,"Peschiera del Garda":null,Petange:null,Peterskirchen:null,"Petit Croix":null,"Pfäffikon SZ":null,Pfarrwerfen:null,Pflach:null,"Pichl b.Schladming":null,Piensk:null,"Pill-Vomperbach":null,"Pino transito":null,"Pisa Centrale":null,"Plesna(CZ)":null,"Ploiesti Vest":null,"Plzen hl.n.":null,"Pöchlarn":null,Poitiers:null,"Pomezi nad Ohri":null,"Pont-a-Mousson":null,Pontresina:null,"Pont-Ste-Maxence":null,"Pont St Vincent":null,Pordenone:null,"Port Vendres Ville":null,"Port Bou":null,"Pörtschach am Wörther See":null,Potucky:null,"Potucky zastavka":null,"Pougues les Eaux":null,"Poznan Gl.":null,"Praha hl.n.":null,"Praha-Holesovice":null,"Praha-Smichov":null,"Pram-Haag":null,Pratteln:null,Predeal:null,Pregarten:null,Prerov:null,Prinzersdorf:null,Profondsart:null,Przylep:null,"Puch bei Hallein":null,Pusarnitz:null,Quevy:null,Quimper:null,Radstadt:null,Rafz:null,Rankweil:null,"Ranzo-S. Abbondio":null,"Rattenberg-Kramsach":null,"Re(I)":null,"Rebstein-Marbach":null,"Réding(F)":null,"Reichenbach im Kandertal":null,Reichenburg:null,Reims:null,"Reith b.Seefeld":null,"Rejsby st":null,Rekawinkel:null,"Rekingen AG":null,"Rémelfing":null,"Rémilly":null,Remiremont:null,Rennes:null,Retenice:null,Retz:null,"Reutte in Tirol":null,"Reutte in Tirol Schulzentrum":null,Rheineck:null,"Rheinfelden(CH)":null,"Ribe st":null,"Ribe Noerremark st":null,Richterswil:null,"Ried im Innkreis":null,"Bregenz Riedenburg":null,"Rietz in Tirol":null,Rimini:null,"Ringsted st":null,"Rio di Pusteria/Mühlbach":null,"Rivera-Bironico":null,Rivesaltes:null,Roanne:null,Robilante:null,Roccavione:null,Rodange:null,Roedby:null,"Roedekro st":null,Roeschwoog:null,"Roggwil-Berg":null,"Roggwil-Wynau":null,"Rohr-Bad Hall":null,Rokycany:null,"Roma Termini":null,Romanshorn:null,"Romanshorn (See)":null,"Rombas-Clouange":null,Ronet:null,"Roodt/Syre":null,Roosendaal:null,Roppen:null,Rorschach:null,"Rorschach Hafen":null,"Rorschach Hafen (See)":null,"Rosenbach bei Villach":null,"Jesenice(Gr)":null,"Roskilde st":null,Rotava:null,"Rothenburg(CH)":null,"Rothenthurm(CH)":null,Rothrist:null,Rotkreuz:null,"Rotterdam Lombardijen":null,"Rotterdam Noord":null,"Rotterdam Centraal":null,"Rotterdam Stadion":null,"Roudnice nad Labem":null,Rouffach:null,Rovereto:null,Rovigo:null,"Rum b.Innsbruck":null,Ruma:null,Rumburk:null,"Rümlang":null,Rupperswil:null,"Rüschlikon":null,Rusova:null,"Rüthi SG":null,Rybniste:null,Rzepin:null,"St-Avre-la-Chambre":null,"St-Michel-Valloire":null,"St.Peter-Seitenstetten":null,"San Candido/Innichen":null,Saalfelden:null,Safenwil:null,Saincaize:null,Saintes:null,"Saint Ghislain":null,"Salez-Sennwald":null,"Salzburg Aiglhof":null,"Salzburg Liefering":null,"Salzburg Aigen":null,"Salzburg Hbf":null,"Salzburg Parsch":null,"Salzburg Sam":null,"Salzburg Süd":null,"Salzburg Gnigl":null,Samedan:null,Samstagern:null,"San Remo":null,"Sandweiler-Contern":null,"Sanry-sur-Nied":null,"Sapjane(Gr)":null,"Sappemeer Oost":null,Sargans:null,Sarrebourg:null,Sarreguemines:null,Sarreinsming:null,"Sathonay Rillieux":null,"Saumur Rive Droit":null,Saverne:null,Savona:null,"Salzburg Mülln-Altstadt":null,"Schaan-Vaduz":null,Schaerbeek:null,Schaffhausen:null,Schaftenau:null,"Schärding":null,Scharnitz:null,Scheemda:null,"Schiedam Centrum":null,Schifflange:null,"Schindellegi-Feusisberg":null,"Schiphol (Airport)":null,Schladming:null,"Schönbichl in Tirol":null,"Schönwies":null,Schouweiler:null,Schruns:null,"Schübelbach-Buttikon":null,Schulen:null,"Schwarzach-St.Veit":null,Schwaz:null,Schwindratzheim:null,Schwyz:null,"Scuol-Tarasp":null,Sebuzin:null,"Seefeld in Tirol":null,"Seekirchen am Wallersee":null,"Sejstrup st":null,"Sélestat":null,Selzthal:null,Seregno:null,"Sesto S. Giovanni":null,Sete:null,Sevelen:null,Sevnica:null,"s-Hertogenbosch":null,"Sid(SRB)":null,"Siebnen-Wangen":null,Siegershausen:null,"Sieniawa Zarska":null,"Sierck-les-Bains":null,"Sierre/Siders":null,Sighisoara:null,Sillian:null,"Silz im Oberinntal":null,Simeria:null,Sinaia:null,Sion:null,Sissach:null,Sittard:null,"Skaerbaek st":null,"Skofja Loka":null,"Slagelse st":null,"Slavonski Brod":null,Slubice:null,Sluknov:null,"Sluknov zast.":null,Smolensk:null,Sneek:null,Sokolov:null,Solothurn:null,Somain:null,Sopot:null,Sopron:null,"Soroe st":null,"Sosnowiec Glowny":null,Spa:null,"Spangsbjerg st":null,Spicak:null,"Spielfeld-Straß":null,Spiez:null,"Spital am Pyhrn":null,"Spittal-Millstättersee":null,"Sremska Mitrovica":null,"St Dalmas de Tende":null,"St. Gallen(CH)":null,"St. Gallen(CH) Winkeln":null,"St. Gallen(CH) Haggen":null,"St-Jory(Toulouse)":null,"St Malo":null,"St.Jodok am Brenner":null,"St. Moritz":null,"St. Anton am Arlberg":null,"St. Anton im Montafon":null,"St. Johann im Pongau":null,"St. Johann in Tirol":null,"St. Margrethen":null,"St.Michael in Obersteiermark":null,"St.Pölten Hbf":null,"St. Valentin":null,"St.Veit/Glan":null,Staad:null,"Wien Stadlau":null,"Stadt Rottenmann":null,Staffelfelden:null,"Stainach-Irdning":null,"St-Amour":null,Stams:null,"Stans bei Schwaz":null,"Stara Pazova":null,"Stara Role":null,Statte:null,"St Avold":null,"Sint-Denijs-Boekel":null,Steckborn:null,"Steckborn URh":null,Steenwijk:null,"Steinach in Tirol":null,Steinbourg:null,"Steindorf bei Straßwalchen":null,Steinebrunn:null,"Stein-Säckingen":null,Stephansfeld:null,Stettbach:null,Steyr:null,"St-Germain-des-Fosses":null,"St-Gervais-les-Bains":null,"St-Hilaire-au-Temple":null,"St-Jean-de-Luz-Ciboure":null,"St-Jean-de-Maurienne Arvan":null,"St-Louis (Haut-Rhin)":null,"St-Maurice(CH)":null,Stockerau:null,"St-Pierre-dAlbigny":null,"St-Pierre-des-Corps":null,"St-Priest":null,"St-Quentin(Aisne)":null,"St-Raphael-Valescure":null,Strasbourg:null,Strasshof:null,"Straßwalchen":null,Stresa:null,"Strizivojna-Vrpolje":null,"St-Sulpice-Lauriere":null,Sturovo:null,Subotica:null,"Nejdek-Sucha":null,Summerau:null,Sursee:null,"Survilliers Fosses":null,"Wien Süßenbrunn":null,Susteren:null,Svatava:null,"Svatava zastavka":null,Svor:null,Swiebodzin:null,"Swinoujscie Centrum":null,"Szczecin Glowny":null,"Szczecin Gumience":null,"Szentgotthárd":null,Szob:null,"Szob(Gr)":null,Szolnok:null,Tabor:null,Taggia:null,Tamines:null,"Tarascon sur Rhone":null,"Tarvisio Boscoverde":null,Tata:null,Tatabanya:null,"Taufkirchen an der Pram":null,"Taulov st":null,"Taverne-Torricella":null,"Taxenbach-Rauris":null,"Salzburg Taxham Europark":null,Tczew:null,Tecknau:null,"Telfs-Pfaffenhofen":null,Templeuve:null,"Tende(F)":null,Tenneck:null,"Teplice v Cechach":null,Terespol:null,"Brest(Gr)":null,"Terfens-Weer":null,Tergnier:null,Testelt:null,"Teting (Moselle)":null,"TGV Haute Picardie":null,Thalwil:null,Thionville:null,Thun:null,Thusis:null,"Tieffenbach-Struth":null,Tienen:null,Tilburg:null,"Tinglev st":null,Tisis:null,"Nejdek-Tisova":null,"Tjaereborg st":null,"Tobel-Affeltrangen":null,"Toender st":null,"Toender Nord st":null,"Tommerup st":null,Tongeren:null,"Torino Porta Susa":null,Toul:null,Toulon:null,"Toulouse-Matabiau":null,Tourcoing:null,Tournai:null,Tournan:null,Tovarnik:null,"Traun OÖ":null,Trbovlje:null,Trebusice:null,"Treibach-Althofen":null,"Trelleborg F":null,Trento:null,"Treviso Centrale":null,Trieben:null,Trimmelkam:null,Troisvierges:null,Trooz:null,Tschagguns:null,"Tulln a.d.Donau":null,Tullnerfeld:null,Tuplice:null,"Tuplice Debinka":null,Turgi:null,Tychy:null,Uckange:null,Udine:null,Uitgeest:null,"Ulmerfeld-Hausmening":null,"Ulrichsbrücke-Füssen":null,"Unterberg-Stefansbrücke":null,Unterterzen:null,Unzmarkt:null,"Usti nad Labem-Strekov":null,"Usti nad Labem hl.n.":null,"Usti nad Labem zapad":null,"Utrecht Centraal":null,Uttwil:null,Vac:null,"Vaires Torcy":null,"Valby(Koebenhavn)":null,"Valdaora-Anterselva/Olang-Antholz":null,Valdek:null,"Val-de-Reuil":null,"Valence Ville":null,"Valence TGV":null,Valenciennes:null,"Valkenburg(NL)":null,"Vamdrup st":null,Vandans:null,"Varangeville-St-Nicolas":null,"Varde st":null,"Varde Kaserne st":null,"Varde Vest st":null,"Varnsdorf stare nadr":null,Varnsdorf:null,"Varnsdorf Pivovar Kocour":null,"Veenendaal-De Klomp":null,Vejprty:null,"Velden am Wörther See":null,"Velke Zernoseky":null,"Velky Senov":null,"Velky Senov zast.":null,Vendenheim:null,"Venezia Mestre":null,"Venezia Santa Lucia":null,Venissieux:null,Venlo:null,Ventimiglia:null,Vercelli:null,Vernante:null,"Verneuil sur Avre":null,"Verneuil lEtang":null,"Vernon(Eure)":null,"Verona Porta Nuova":null,"Verviers Central":null,"Verviers-Palais":null,"Vesele pod Rabstejnem":null,Wjasma:null,"Viby Sjaelland st":null,Vicenza:null,Vienne:null,Vievola:null,"Vilemov u Sluknova":null,"Villabassa-Braies/Niederdorf-Prags":null,"Villach Hbf":null,"Villach Westbf":null,"Villars les Dombes":null,"Villedieu les Poeles":null,"Villers Cotterets":null,"Villiers-le-Bel-Gonesse":null,"Vils in Tirol":null,Vilvoorde:null,Vinkovci:null,"Vipiteno-Val di Vizze/Sterzing-Pfitsch":null,"Visby st":null,Vise:null,Visp:null,"Vitry le François Gare":null,Vittel:null,Vleuten:null,Vlissingen:null,"Vöcklabruck":null,"Vöcklamarkt":null,Voghera:null,"Vojens st":null,Vojtanov:null,"Volders-Baumkirchen":null,"Völs":null,"Vordingborg st":null,"Vroegum st":null,"Vysoka Pec":null,"Wädenswil":null,"Wald am Schoberpass":null,Walenstadt:null,Walferdange:null,Wallisellen:null,"Villach Warmbad":null,"Warszawa Centralna":null,"Warszawa Wschodnia":null,"Warszawa Zachodnia":null,"Wartberg/Krems":null,"Wartberg im Mürztal":null,Wasserbillig:null,Waterloo:null,"Watermael/Watermaal":null,Wecker:null,Weert:null,Weesp:null,Wegliniec:null,Weinfelden:null,Welkenraedt:null,"Wels Hbf":null,"Wendling b.Haag":null,Werfen:null,Wernstein:null,"Wespelaar-Tildonk":null,"Westendorf in Tirol":null,Wettingen:null,"Wien Franz-Josefs-Bahnhof":null,"Flughafen Wien":null,"Wien Hbf":null,"Wien Hbf (Autoreisezuganlage)":null,"Wien Hütteldorf":null,"Wien Meidling":null,"Wien Mitte":null,"Wien Praterstern":null,"Wien Simmering":null,"Wien Westbahnhof":null,"Wiener Neustadt Hbf":null,"Wien Penzing":null,"Wil SG":null,Wildon:null,Wilwerwiltz:null,Wilwisheim:null,Windischgarsten:null,"Wingen-sur-Moder":null,Winschoten:null,Winterswijk:null,Winterthur:null,Wissembourg:null,Witnica:null,Wittenbach:null,Wittring:null,Woerden:null,"Wohlen AG":null,Woippy:null,Wolfstee:null,Wolfurt:null,"Wolkersdorf im Weinviertel":null,"Wörgl Hbf":null,Wormerveer:null,"Wroclaw Glowny":null,"Wroclaw Lesnica":null,"Wroclaw Nowy Dwor":null,"Ybbs a.d. Donau":null,"Yverdon-les-Bains":null,"Yves-Gomezee":null,Zagan:null,Zagorje:null,"Zagreb Glavni kolodvor":null,"Zandvoort aan Zee":null,Zary:null,Zasieki:null,Zawiercie:null,Zbaszynek:null,Zebrzydowice:null,Zedelgem:null,"Zeebrugge-Dorp":null,"Zelezna Ruda centrum":null,"Zelezna Ruda mesto":null,"Zell am See":null,"Zell am Ziller":null,Zeltweg:null,Zermatt:null,Zetting:null,Zevenaar:null,Zgorzelec:null,"Zgorzelec Miasto":null,Zichem:null,"Zidani Most":null,"Ziegelbrücke":null,"Zielona Gora Gl.":null,Zirl:null,"Zirovice-Seniky":null,Zizers:null,Zofingen:null,Zolder:null,"Zug(CH)":null,Zuidbroek:null,Zumarraga:null,"Zürich Enge":null,"Zürich Flughafen":null,"Zürich HB":null,"Zürich Wiedikon":null,"Zürich Wollishofen":null,"Zürich Altstetten":null,"Zürich Hardbrücke":null,"Zürich Oerlikon":null,Opfikon:null,"Zürich Stadelhofen":null,"Zwijndrecht(NL)":null,Zwolle:null}})});
diff --git a/public/static/js/geolocation.js b/public/static/js/geolocation.js
index cb16e72..03857a1 100644
--- a/public/static/js/geolocation.js
+++ b/public/static/js/geolocation.js
@@ -1,50 +1,71 @@
+/*
+ * Copyright (C) 2020 Birte Kristina Friesel
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
$(document).ready(function() {
- function getPlaceholder() {
+ const getPlaceholder = function() {
return $('div.geolocation div.progress');
}
- var showError = function(header, message, code) {
- getPlaceholder().remove();
- var errnode = $(document.createElement('div'));
+ const showError = function(header, message, code) {
+ const errnode = $(document.createElement('div'));
errnode.attr('class', 'error');
errnode.text(message);
- var headnode = $(document.createElement('strong'));
- headnode.text(header);
+ const headnode = $(document.createElement('strong'));
+ headnode.text(header + ' ');
errnode.prepend(headnode);
$('div.geolocation').append(errnode);
+
+ const recent = $('div.geolocation').data('recent');
+ if (recent) {
+ const stops = recent.split('|');
+ const res = $(document.createElement('p'));
+ $.each(stops, function(i, stop) {
+ const parts = stop.split(';');
+ const node = $('<a class="tablerow" href="/s/' + parts[0] + '?hafas=' + parts[2] + '"><span><i class="material-icons" aria-hidden="true">' + (parseInt(parts[2]) ? 'directions' : 'train') + '</i>' + parts[1] + '</span></a>');
+ node.click(function() {
+ $('nav .preloader-wrapper').addClass('active');
+ });
+ res.append(node);
+ });
+ $('p.geolocationhint').text('Letzte Ziele:');
+ getPlaceholder().replaceWith(res);
+ } else {
+ getPlaceholder().remove();
+ }
};
- var processResult = function(data) {
+ const processResult = function(data) {
if (data.error) {
showError('Backend-Fehler:', data.error, null);
} else if (data.candidates.length == 0) {
showError('Keine Bahnhöfe in 70km Umkreis gefunden', '', null);
} else {
- resultTable = $('<table><tbody></tbody></table>')
- resultBody = resultTable.children();
+ const res = $(document.createElement('p'));
$.each(data.candidates, function(i, candidate) {
- var ds100 = candidate.ds100,
+ const eva = candidate.eva,
name = candidate.name,
- distance = candidate.distance;
- distance = distance.toFixed(1);
-
- var stationlink = $(document.createElement('a'));
- stationlink.attr('href', ds100);
- stationlink.text(name);
+ hafas = candidate.hafas,
+ distance = candidate.distance.toFixed(1);
- resultBody.append('<tr><td><a href="/s/' + ds100 + '">' + name + '</a></td></tr>');
+ const node = $('<a class="tablerow" href="/s/' + eva + '?hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">' + (parseInt(hafas) ? 'directions' : 'train') + '</i>' + name + '</span></a>');
+ node.click(function() {
+ $('nav .preloader-wrapper').addClass('active');
+ });
+ res.append(node);
});
- getPlaceholder().replaceWith(resultTable);
+ getPlaceholder().replaceWith(res);
}
};
- var processLocation = function(loc) {
+ const processLocation = function(loc) {
$.post('/geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude}, processResult);
};
- var processError = function(error) {
+ const processError = function(error) {
if (error.code == error.PERMISSION_DENIED) {
showError('Standortanfrage nicht möglich.', 'Vermutlich fehlen die Rechte im Browser oder der Android Location Service ist deaktiviert.', 'geolocation.error.PERMISSION_DENIED');
} else if (error.code == error.POSITION_UNAVAILABLE) {
@@ -56,8 +77,9 @@ $(document).ready(function() {
}
};
- var geoLocationButton = $('div.geolocation > button');
- var getGeoLocation = function() {
+ const geoLocationButton = $('div.geolocation > button');
+ const recentStops = geoLocationButton.data('recent');
+ const getGeoLocation = function() {
geoLocationButton.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>'));
navigator.geolocation.getCurrentPosition(processLocation, processError);
}
diff --git a/public/static/js/geolocation.min.js b/public/static/js/geolocation.min.js
index ab832e2..54633f8 100644
--- a/public/static/js/geolocation.min.js
+++ b/public/static/js/geolocation.min.js
@@ -1 +1 @@
-$(document).ready(function(){function e(){return $("div.geolocation div.progress")}var t=function(t,n,o){e().remove();var r=$(document.createElement("div"));r.attr("class","error"),r.text(n);var i=$(document.createElement("strong"));i.text(t),r.prepend(i),$("div.geolocation").append(r)},n=function(n){n.error?t("Backend-Fehler:",n.error):0==n.candidates.length?t("Keine Bahnhöfe in 70km Umkreis gefunden",""):(resultTable=$("<table><tbody></tbody></table>"),resultBody=resultTable.children(),$.each(n.candidates,function(e,t){var n=t.ds100,o=t.name,r=t.distance;r=r.toFixed(1);var i=$(document.createElement("a"));i.attr("href",n),i.text(o),resultBody.append('<tr><td><a href="/s/'+n+'">'+o+"</a></td></tr>")}),e().replaceWith(resultTable))},o=function(e){$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},n)},r=function(e){e.code==e.PERMISSION_DENIED?t("Standortanfrage nicht möglich.","Vermutlich fehlen die Rechte im Browser oder der Android Location Service ist deaktiviert."):e.code==e.POSITION_UNAVAILABLE?t("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)"):e.code==e.TIMEOUT?t("Standort konnte nicht ermittelt werden","(Timeout)"):t("Standort konnte nicht ermittelt werden","(unbekannter Fehler)")},i=$("div.geolocation > button"),a=function(){i.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>')),navigator.geolocation.getCurrentPosition(o,r)};i.length&&(navigator.geolocation?navigator.permissions?navigator.permissions.query({name:"geolocation"}).then(function(e){"prompt"===e.state?i.on("click",a):a()}):i.on("click",a):t("Standortanfragen werden von diesem Browser nicht unterstützt",""))});
+$(document).ready(function(){function r(){return $("div.geolocation div.progress")}function e(e){$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},o)}function n(e){e.code==e.PERMISSION_DENIED?t("Standortanfrage nicht möglich.","Vermutlich fehlen die Rechte im Browser oder der Android Location Service ist deaktiviert.","geolocation.error.PERMISSION_DENIED"):e.code==e.POSITION_UNAVAILABLE?t("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?t("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):t("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}const t=function(e,n,t){var o=$(document.createElement("div")),n=(o.attr("class","error"),o.text(n),$(document.createElement("strong"))),e=(n.text(e+" "),o.prepend(n),$("div.geolocation").append(o),$("div.geolocation").data("recent"));if(e){n=e.split("|");const a=$(document.createElement("p"));$.each(n,function(e,n){n=n.split(";"),n=$('<a class="tablerow" href="/s/'+n[0]+"?hafas="+n[2]+'"><span><i class="material-icons" aria-hidden="true">'+(parseInt(n[2])?"directions":"train")+"</i>"+n[1]+"</span></a>");n.click(function(){$("nav .preloader-wrapper").addClass("active")}),a.append(n)}),$("p.geolocationhint").text("Letzte Ziele:"),r().replaceWith(a)}else r().remove()},o=function(e){if(e.error)t("Backend-Fehler:",e.error,null);else if(0==e.candidates.length)t("Keine Bahnhöfe in 70km Umkreis gefunden","",null);else{const i=$(document.createElement("p"));$.each(e.candidates,function(e,n){var t=n.eva,o=n.name,a=n.hafas,n=(n.distance.toFixed(1),$('<a class="tablerow" href="/s/'+t+"?hafas="+a+'"><span><i class="material-icons" aria-hidden="true">'+(parseInt(a)?"directions":"train")+"</i>"+o+"</span></a>"));n.click(function(){$("nav .preloader-wrapper").addClass("active")}),i.append(n)}),r().replaceWith(i)}},a=$("div.geolocation > button");a.data("recent");function i(){a.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>')),navigator.geolocation.getCurrentPosition(e,n)}a.length&&(navigator.geolocation?navigator.permissions?navigator.permissions.query({name:"geolocation"}).then(function(e){"prompt"===e.state?a.on("click",i):i()}):a.on("click",i):t("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js
index f42b491..48e878f 100644
--- a/public/static/js/travelynx-actions.js
+++ b/public/static/js/travelynx-actions.js
@@ -1,10 +1,29 @@
+/*
+ * Copyright (C) 2020 Birte Kristina Friesel
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
var j_departure = 0;
var j_duration = 0;
var j_arrival = 0;
var j_dest = '';
var j_stops = [];
+var j_token = '';
+
+function setTheme(theme) {
+ localStorage.setItem('theme', theme);
+ if (!otherTheme.hasOwnProperty(theme)) {
+ theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ }
+ addStyleSheet(theme, 'theme');
+}
+
function upd_journey_data() {
$('.countdown').each(function() {
+ const journey_token = $(this).data('token');
+ if (journey_token) {
+ j_token = journey_token;
+ }
var journey_data = $(this).data('journey');
if (journey_data) {
journey_data = journey_data.split(';');
@@ -31,12 +50,19 @@ function upd_journey_data() {
});
}
function upd_countdown() {
- var now = Date.now() / 1000;
+ const now = Date.now() / 1000;
if (j_departure > now) {
$('.countdown').text('Abfahrt in ' + Math.round((j_departure - now)/60) + ' Minuten');
} else if (j_arrival > 0) {
if (j_arrival > now) {
- $('.countdown').text('Ankunft in ' + Math.round((j_arrival - now)/60) + ' Minuten');
+ var diff = Math.round((j_arrival - now)/60);
+ if (diff >= 120) {
+ $('.countdown').text('Ankunft in ' + Math.floor(diff/60) + ' Stunden und ' + (diff%60) + ' Minuten');
+ } else if (diff >= 60) {
+ $('.countdown').text('Ankunft in 1 Stunde und ' + (diff%60) + ' Minuten');
+ } else {
+ $('.countdown').text('Ankunft in ' + diff + ' Minuten');
+ }
} else {
$('.countdown').text('Ziel erreicht');
}
@@ -60,7 +86,13 @@ function odelay(sched, rt) {
function tvly_run(link, req, err_callback) {
var error_icon = '<i class="material-icons">error</i>';
- var progressbar = $('<div class="progress"><div class="indeterminate"></div></div>');
+ var progressbar;
+ if (link.data('tr')) {
+ progressbar = $('<tr><td colspan="' + link.data('tr') + '"><div class="progress"><div class="indeterminate"></div></div></td></tr>');
+ }
+ else {
+ progressbar = $('<div class="progress"><div class="indeterminate"></div></div>');
+ }
link.hide();
link.after(progressbar);
$.post('/action', req, function(data) {
@@ -91,10 +123,12 @@ function tvly_update() {
}
function tvly_update_public() {
var user_name;
+ var profile_status = 0;
$('.publicstatuscol').each(function() {
user_name = $(this).data('user');
+ profile_status = $(this).data('profile');
});
- $.get('/ajax/status/' + user_name + '.html', function(data) {
+ $.get('/ajax/status/' + user_name + '.html', {token: j_token, profile: profile_status}, function(data) {
$('.publicstatuscol').html(data);
upd_journey_data();
setTimeout(tvly_update_public, 40000);
@@ -104,6 +138,15 @@ function tvly_update_public() {
setTimeout(tvly_update_public, 5000);
});
}
+function tvly_update_timeline() {
+ $.get('/timeline/in-transit', {ajax: 1}, function(data) {
+ $('.timeline-in-transit').html(data);
+ setTimeout(tvly_update_timeline, 60000);
+ }).fail(function() {
+ $('.sync-failed-marker').css('display', 'block');
+ setTimeout(tvly_update_timeline, 10000);
+ });
+}
function tvly_journey_progress() {
var now = Date.now() / 1000;
var progress = 0;
@@ -132,7 +175,11 @@ function tvly_journey_progress() {
break;
}
if ((rt_dep != 0) && (rt_dep - now > 0)) {
- $('.next-stop').html(stop_name + '<br/>' + hhmm(rt_arr) + ' → ' + hhmm(rt_dep) + odelay(sched_dep, rt_dep));
+ if (rt_arr != 0) {
+ $('.next-stop').html(stop_name + '<br/>' + hhmm(rt_arr) + ' → ' + hhmm(rt_dep) + odelay(sched_dep, rt_dep));
+ } else {
+ $('.next-stop').html(stop_name + '<br/>' + hhmm(rt_dep) + odelay(sched_dep, rt_dep));
+ }
break;
}
}
@@ -147,6 +194,7 @@ function tvly_reg_handlers() {
station: link.data('station'),
train: link.data('train'),
dest: link.data('dest'),
+ ts: link.data('ts'),
};
tvly_run(link, req);
});
@@ -158,8 +206,10 @@ function tvly_reg_handlers() {
force: link.data('force'),
};
tvly_run(link, req, function() {
- link.append(' – Ohne Echtzeitdaten auschecken?')
- link.data('force', true);
+ if (!link.data('force')) {
+ link.append(' – Ohne Echtzeitdaten auschecken?')
+ link.data('force', true);
+ }
});
});
$('.action-undo').click(function() {
@@ -183,6 +233,7 @@ function tvly_reg_handlers() {
var req = {
action: 'cancelled_from',
station: link.data('station'),
+ ts: link.data('ts'),
train: link.data('train'),
};
tvly_run(link, req);
@@ -204,7 +255,7 @@ function tvly_reg_handlers() {
checkin: link.data('checkin'),
checkout: link.data('checkout'),
};
- var really_delete = confirm("Diese Zugfahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.");
+ var really_delete = confirm("Diese Fahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.");
if (really_delete) {
tvly_run(link, req);
}
@@ -250,7 +301,29 @@ $(document).ready(function() {
setTimeout(tvly_update_public, 40000);
setTimeout(tvly_journey_progress, 5000);
}
+ if ($('.timeline-in-transit .autorefresh').length) {
+ setTimeout(tvly_update_timeline, 60000);
+ }
$('a[href]').click(function() {
$('nav .preloader-wrapper').addClass('active');
});
+ $('a[href="#now"]').keydown(function(event) {
+ // also trigger click handler on keyboard enter
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ event.target.click();
+ }
+ });
+ $('a[href="#now"]').click(function(event) {
+ event.preventDefault();
+ $('nav .preloader-wrapper').removeClass('active');
+ now_el = $('#now')[0];
+ now_el.previousElementSibling.querySelector(".dep-time").focus();
+ now_el.scrollIntoView({behavior: "smooth", block: "center"});
+ });
+ const elems = document.querySelectorAll('.carousel');
+ const instances = M.Carousel.init(elems, {
+ fullWidth: true,
+ indicators: true}
+ );
});
diff --git a/public/static/js/travelynx-actions.min.js b/public/static/js/travelynx-actions.min.js
index 9efef3f..8b99a82 100644
--- a/public/static/js/travelynx-actions.min.js
+++ b/public/static/js/travelynx-actions.min.js
@@ -1 +1 @@
-function upd_journey_data(){$(".countdown").each(function(){var t=$(this).data("journey");t&&(t=t.split(";"),j_departure=parseInt(t[0]),j_arrival=parseInt(t[1]),j_duration=j_arrival-j_departure);var e=$(this).data("dest");e&&(j_dest=e);var a=$(this).data("route");if(a){a=a.split("|"),j_stops=[];for(var n in a){for(var o=a[n].split(";"),r=1;r<5;r++)o[r]=parseInt(o[r]);j_stops.push(o)}}})}function upd_countdown(){var t=Date.now()/1e3;j_departure>t?$(".countdown").text("Abfahrt in "+Math.round((j_departure-t)/60)+" Minuten"):j_arrival>0&&(j_arrival>t?$(".countdown").text("Ankunft in "+Math.round((j_arrival-t)/60)+" Minuten"):$(".countdown").text("Ziel erreicht"))}function hhmm(t){var e=new Date(1e3*t),a=e.getHours(),n=e.getMinutes();return(a<10?"0"+a:a)+":"+(n<10?"0"+n:n)}function odelay(t,e){return t<e?" (+"+(e-t)/60+")":t==e?"":" ("+(e-t)/60+")"}function tvly_run(t,e,a){var n='<i class="material-icons">error</i>',o=$('<div class="progress"><div class="indeterminate"></div></div>');t.hide(),t.after(o),$.post("/action",e,function(e){e.success?$(location).attr("href",e.redirect_to):(M.toast({html:n+" "+e.error}),o.remove(),a&&a(),t.append(" "+n),t.show())})}function tvly_update(){$.get("/ajax/status_card.html",function(t){$(".statuscol").html(t),tvly_reg_handlers(),upd_journey_data(),setTimeout(tvly_update,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update,5e3)})}function tvly_update_public(){var t;$(".publicstatuscol").each(function(){t=$(this).data("user")}),$.get("/ajax/status/"+t+".html",function(t){$(".publicstatuscol").html(t),upd_journey_data(),setTimeout(tvly_update_public,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update_public,5e3)})}function tvly_journey_progress(){var t=Date.now()/1e3,e=0;if(j_duration>0){e=1-(j_arrival-t)/j_duration,e<0&&(e=0),e>1&&(e=1),$(".progress .determinate").css("width",100*e+"%");for(stop in j_stops){var a=j_stops[stop][0],n=j_stops[stop][1],o=j_stops[stop][2],r=j_stops[stop][3],i=j_stops[stop][4];if(a==j_dest){$(".next-stop").html("");break}if(0!=o&&o-t>0){$(".next-stop").html(a+"<br/>"+hhmm(o)+odelay(n,o));break}if(0!=i&&i-t>0){$(".next-stop").html(a+"<br/>"+hhmm(o)+" → "+hhmm(i)+odelay(r,i));break}}setTimeout(tvly_journey_progress,5e3)}}function tvly_reg_handlers(){$(".action-checkin").click(function(){var t=$(this);tvly_run(t,{action:"checkin",station:t.data("station"),train:t.data("train"),dest:t.data("dest")})}),$(".action-checkout").click(function(){var t=$(this),e={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,e,function(){t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0)})}),$(".action-undo").click(function(){var t=$(this),e=Date.now()/1e3,a=parseInt(t.data("checkints")),n={action:"undo",undo_id:t.data("id")},o=!0;e-a>900&&(o=confirm("Checkin wirklich rückgängig machen? Er kann ggf. nicht wiederholt werden.")),o&&tvly_run(t,n)}),$(".action-cancelled-from").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_from",station:t.data("station"),train:t.data("train")})}),$(".action-cancelled-to").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_to",station:t.data("station"),force:!0})}),$(".action-delete").click(function(){var t=$(this),e={action:"delete",id:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};confirm("Diese Zugfahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.")&&tvly_run(t,e)}),$(".action-share").click(function(){var t=$(this).data("text"),e=$(this).data("url");if(navigator.share)shareObj={text:t},e&&(shareObj.url=e),navigator.share(shareObj);else{var a=document.createElement("textarea");e&&(t+=" "+e),a.value=t,a.setAttribute("readonly",""),a.style.position="absolute",a.style.left="-9999px",document.body.appendChild(a),a.select(),a.setSelectionRange(0,99999),document.execCommand("copy"),document.body.removeChild(a),M.toast({html:"Text kopiert: „"+t+"“"})}})}var j_departure=0,j_duration=0,j_arrival=0,j_dest="",j_stops=[];$(document).ready(function(){tvly_reg_handlers(),$(".statuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update,4e4),setTimeout(tvly_journey_progress,5e3)),$(".publicstatuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update_public,4e4),setTimeout(tvly_journey_progress,5e3)),$("a[href]").click(function(){$("nav .preloader-wrapper").addClass("active")})});
+var j_departure=0,j_duration=0,j_arrival=0,j_dest="",j_stops=[],j_token="";function setTheme(t){localStorage.setItem("theme",t),otherTheme.hasOwnProperty(t)||(t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"),addStyleSheet(t,"theme")}function upd_journey_data(){$(".countdown").each(function(){var t=$(this).data("token"),t=(t&&(j_token=t),$(this).data("journey")),t=(t&&(t=t.split(";"),j_departure=parseInt(t[0]),j_arrival=parseInt(t[1]),j_duration=j_arrival-j_departure),$(this).data("dest")),e=(t&&(j_dest=t),$(this).data("route"));if(e)for(var a in e=e.split("|"),j_stops=[],e){for(var n=e[a].split(";"),o=1;o<5;o++)n[o]=parseInt(n[o]);j_stops.push(n)}})}function upd_countdown(){var t=Date.now()/1e3;t<j_departure?$(".countdown").text("Abfahrt in "+Math.round((j_departure-t)/60)+" Minuten"):0<j_arrival&&(t<j_arrival?120<=(t=Math.round((j_arrival-t)/60))?$(".countdown").text("Ankunft in "+Math.floor(t/60)+" Stunden und "+t%60+" Minuten"):60<=t?$(".countdown").text("Ankunft in 1 Stunde und "+t%60+" Minuten"):$(".countdown").text("Ankunft in "+t+" Minuten"):$(".countdown").text("Ziel erreicht"))}function hhmm(t){var t=new Date(1e3*t),e=t.getHours(),t=t.getMinutes();return(e<10?"0"+e:e)+":"+(t<10?"0"+t:t)}function odelay(t,e){return t<e?" (+"+(e-t)/60+")":t==e?"":" ("+(e-t)/60+")"}function tvly_run(e,t,a){var n='<i class="material-icons">error</i>',o=e.data("tr")?$('<tr><td colspan="'+e.data("tr")+'"><div class="progress"><div class="indeterminate"></div></div></td></tr>'):$('<div class="progress"><div class="indeterminate"></div></div>');e.hide(),e.after(o),$.post("/action",t,function(t){t.success?$(location).attr("href",t.redirect_to):(M.toast({html:n+" "+t.error}),o.remove(),a&&a(),e.append(" "+n),e.show())})}function tvly_update(){$.get("/ajax/status_card.html",function(t){$(".statuscol").html(t),tvly_reg_handlers(),upd_journey_data(),setTimeout(tvly_update,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update,5e3)})}function tvly_update_public(){var t,e=0;$(".publicstatuscol").each(function(){t=$(this).data("user"),e=$(this).data("profile")}),$.get("/ajax/status/"+t+".html",{token:j_token,profile:e},function(t){$(".publicstatuscol").html(t),upd_journey_data(),setTimeout(tvly_update_public,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update_public,5e3)})}function tvly_update_timeline(){$.get("/timeline/in-transit",{ajax:1},function(t){$(".timeline-in-transit").html(t),setTimeout(tvly_update_timeline,6e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),setTimeout(tvly_update_timeline,1e4)})}function tvly_journey_progress(){var t=Date.now()/1e3,e=0;if(0<j_duration){for(stop in 1<(e=(e=1-(j_arrival-t)/j_duration)<0?0:e)&&(e=1),$(".progress .determinate").css("width",100*e+"%"),j_stops){var a=j_stops[stop][0],n=j_stops[stop][1],o=j_stops[stop][2],i=j_stops[stop][3],r=j_stops[stop][4];if(a==j_dest){$(".next-stop").html("");break}if(0!=o&&0<o-t){$(".next-stop").html(a+"<br/>"+hhmm(o)+odelay(n,o));break}if(0!=r&&0<r-t){0!=o?$(".next-stop").html(a+"<br/>"+hhmm(o)+" → "+hhmm(r)+odelay(i,r)):$(".next-stop").html(a+"<br/>"+hhmm(r)+odelay(i,r));break}}setTimeout(tvly_journey_progress,5e3)}}function tvly_reg_handlers(){$(".action-checkin").click(function(){var t=$(this),e={action:"checkin",station:t.data("station"),train:t.data("train"),dest:t.data("dest"),ts:t.data("ts")};tvly_run(t,e)}),$(".action-checkout").click(function(){var t=$(this),e={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,e,function(){t.data("force")||(t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0))})}),$(".action-undo").click(function(){var t=$(this),e=Date.now()/1e3,a=parseInt(t.data("checkints")),n={action:"undo",undo_id:t.data("id")},o=!0;(o=900<e-a?confirm("Checkin wirklich rückgängig machen? Er kann ggf. nicht wiederholt werden."):o)&&tvly_run(t,n)}),$(".action-cancelled-from").click(function(){var t=$(this),e={action:"cancelled_from",station:t.data("station"),ts:t.data("ts"),train:t.data("train")};tvly_run(t,e)}),$(".action-cancelled-to").click(function(){var t=$(this),e={action:"cancelled_to",station:t.data("station"),force:!0};tvly_run(t,e)}),$(".action-delete").click(function(){var t=$(this),e={action:"delete",id:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};confirm("Diese Fahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.")&&tvly_run(t,e)}),$(".action-share").click(function(){var t=$(this).data("text"),e=$(this).data("url");navigator.share?(shareObj={text:t},e&&(shareObj.url=e),navigator.share(shareObj)):(e&&(t+=" "+e),(e=document.createElement("textarea")).value=t,e.setAttribute("readonly",""),e.style.position="absolute",e.style.left="-9999px",document.body.appendChild(e),e.select(),e.setSelectionRange(0,99999),document.execCommand("copy"),document.body.removeChild(e),M.toast({html:"Text kopiert: „"+t+"“"}))})}$(document).ready(function(){tvly_reg_handlers(),$(".statuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update,4e4),setTimeout(tvly_journey_progress,5e3)),$(".publicstatuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update_public,4e4),setTimeout(tvly_journey_progress,5e3)),$(".timeline-in-transit .autorefresh").length&&setTimeout(tvly_update_timeline,6e4),$("a[href]").click(function(){$("nav .preloader-wrapper").addClass("active")}),$('a[href="#now"]').keydown(function(t){13==t.keyCode&&(t.preventDefault(),t.target.click())}),$('a[href="#now"]').click(function(t){t.preventDefault(),$("nav .preloader-wrapper").removeClass("active"),(now_el=$("#now")[0]).previousElementSibling.querySelector(".dep-time").focus(),now_el.scrollIntoView({behavior:"smooth",block:"center"})});var t=document.querySelectorAll(".carousel");M.Carousel.init(t,{fullWidth:!0,indicators:!0})});
diff --git a/public/static/manifest.json b/public/static/manifest.json
index 5a44a3f..ed2760a 100644
--- a/public/static/manifest.json
+++ b/public/static/manifest.json
@@ -3,27 +3,27 @@
"short_name": "Travelynx",
"scope": "/",
"icons": [{
- "src": "/static/v32/icons/icon-128x128.png",
+ "src": "/static/v72/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
- "src": "/static/v32/icons/icon-144x144.png",
+ "src": "/static/v72/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
- "src": "/static/v32/icons/icon-152x152.png",
+ "src": "/static/v72/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
- "src": "/static/v32/icons/icon-192x192.png",
+ "src": "/static/v72/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
- "src": "/static/v32/icons/icon-256x256.png",
+ "src": "/static/v72/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}, {
- "src": "/static/v32/icons/icon-512x512.png",
+ "src": "/static/v72/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
diff --git a/public/static/v31 b/public/static/v71
index 945c9b4..945c9b4 120000
--- a/public/static/v31
+++ b/public/static/v71
diff --git a/public/static/v32 b/public/static/v72
index 945c9b4..945c9b4 120000
--- a/public/static/v32
+++ b/public/static/v72
diff --git a/sass/.prettierrc b/sass/.prettierrc
deleted file mode 100644
index 015eea5..0000000
--- a/sass/.prettierrc
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "singleQuote": true,
- "tabWidth": 2,
- "printWidth": 80,
- "semi": true,
- "trailingComma": "es5"
-}
diff --git a/sass/components/_badges.scss b/sass/components/_badges.scss
new file mode 100644
index 0000000..ffed87d
--- /dev/null
+++ b/sass/components/_badges.scss
@@ -0,0 +1,55 @@
+// Badges
+span.badge {
+ min-width: 3rem;
+ padding: 0 6px;
+ margin-left: 14px;
+ text-align: center;
+ font-size: 1rem;
+ line-height: $badge-height;
+ height: $badge-height;
+ color: color('grey', 'darken-1');
+ float: right;
+ box-sizing: border-box;
+
+ &.new {
+ font-weight: 300;
+ font-size: 0.8rem;
+ color: #fff;
+ background-color: $badge-bg-color;
+ border-radius: 2px;
+ }
+ &.new:after {
+ content: " new";
+ }
+
+ &[data-badge-caption]::after {
+ content: " " attr(data-badge-caption);
+ }
+}
+
+// Special cases
+nav ul a span.badge {
+ display: inline-block;
+ float: none;
+ margin-left: 4px;
+ line-height: $badge-height;
+ height: $badge-height;
+ -webkit-font-smoothing: auto;
+}
+
+// Line height centering
+.collection-item span.badge {
+ margin-top: calc(#{$collection-line-height / 2} - #{$badge-height / 2});
+}
+.collapsible span.badge {
+ margin-left: auto;
+}
+.sidenav span.badge {
+ margin-top: calc(#{$sidenav-line-height / 2} - #{$badge-height / 2});
+}
+
+table span.badge {
+ display: inline-block;
+ float: none;
+ margin-left: auto;
+}
diff --git a/sass/components/_buttons.scss b/sass/components/_buttons.scss
new file mode 100644
index 0000000..44b80c8
--- /dev/null
+++ b/sass/components/_buttons.scss
@@ -0,0 +1,322 @@
+// shared styles
+.btn,
+.btn-flat {
+ border: $button-border;
+ border-radius: $button-radius;
+ display: inline-block;
+ height: $button-height;
+ line-height: $button-height;
+ padding: $button-padding;
+ text-transform: uppercase;
+ vertical-align: middle;
+ -webkit-tap-highlight-color: transparent; // Gets rid of tap active state
+}
+
+// Disabled shared style
+.btn.disabled,
+.btn-floating.disabled,
+.btn-large.disabled,
+.btn-small.disabled,
+.btn-flat.disabled,
+.btn:disabled,
+.btn-floating:disabled,
+.btn-large:disabled,
+.btn-small:disabled,
+.btn-flat:disabled,
+.btn[disabled],
+.btn-floating[disabled],
+.btn-large[disabled],
+.btn-small[disabled],
+.btn-flat[disabled] {
+ pointer-events: none;
+ background-color: $button-disabled-background !important;
+ box-shadow: none;
+ color: $button-disabled-color !important;
+ cursor: default;
+ &:hover {
+ background-color: $button-disabled-background !important;
+ color: $button-disabled-color !important;
+ }
+}
+
+// Shared icon styles
+.btn,
+.btn-floating,
+.btn-large,
+.btn-small,
+.btn-flat {
+ font-size: $button-font-size;
+ outline: 0;
+ i {
+ font-size: $button-icon-font-size;
+ line-height: inherit;
+ }
+}
+
+// Shared focus button style
+.btn,
+.btn-floating {
+ &:focus {
+ background-color: darken($button-raised-background, 10%);
+ }
+}
+
+// Raised Button
+.btn {
+ text-decoration: none;
+ color: $button-raised-color;
+ background-color: $button-raised-background;
+ text-align: center;
+ letter-spacing: .5px;
+ @extend .z-depth-1;
+ transition: background-color .2s ease-out;
+ cursor: pointer;
+ &:hover {
+ background-color: $button-raised-background-hover;
+ @extend .z-depth-1-half;
+ }
+}
+
+// Floating button
+.btn-floating {
+ &:hover {
+ background-color: $button-floating-background-hover;
+ @extend .z-depth-1-half;
+ }
+ &:before {
+ border-radius: 0;
+ }
+ &.btn-large {
+ &.halfway-fab {
+ bottom: -$button-floating-large-size / 2;
+ }
+ width: $button-floating-large-size;
+ height: $button-floating-large-size;
+ padding: 0;
+ i {
+ line-height: $button-floating-large-size;
+ }
+ }
+
+ &.btn-small {
+ &.halfway-fab {
+ bottom: -$button-floating-small-size / 2;
+ }
+ width: $button-floating-small-size;
+ height: $button-floating-small-size;
+ i {
+ line-height: $button-floating-small-size;
+ }
+ }
+
+ &.halfway-fab {
+ &.left {
+ right: auto;
+ left: 24px;
+ }
+ position: absolute;
+ right: 24px;
+ bottom: -$button-floating-size / 2;
+ }
+ display: inline-block;
+ color: $button-floating-color;
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+ width: $button-floating-size;
+ height: $button-floating-size;
+ line-height: $button-floating-size;
+ padding: 0;
+ background-color: $button-floating-background;
+ border-radius: $button-floating-radius;
+ @extend .z-depth-1;
+ transition: background-color .3s;
+ cursor: pointer;
+ vertical-align: middle;
+ i {
+ width: inherit;
+ display: inline-block;
+ text-align: center;
+ color: $button-floating-color;
+ font-size: $button-large-icon-font-size;
+ line-height: $button-floating-size;
+ }
+}
+
+// button fix
+button.btn-floating {
+ border: $button-border;
+}
+
+// Fixed Action Button
+.fixed-action-btn {
+ &.active {
+ ul {
+ visibility: visible;
+ }
+ }
+
+ // Directions
+ &.direction-left,
+ &.direction-right {
+ padding: 0 0 0 15px;
+ ul {
+ text-align: right;
+ right: 64px;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 100%;
+ left: auto;
+ /*width 100% only goes to width of button container */
+ width: 500px;
+ li {
+ display: inline-block;
+ margin: 7.5px 15px 0 0;
+ }
+ }
+ }
+ &.direction-right {
+ padding: 0 15px 0 0;
+ ul {
+ text-align: left;
+ direction: rtl;
+ left: 64px;
+ right: auto;
+ li {
+ margin: 7.5px 0 0 15px;
+ }
+ }
+ }
+ &.direction-bottom {
+ padding: 0 0 15px 0;
+ ul {
+ top: 64px;
+ bottom: auto;
+ display: flex;
+ flex-direction: column-reverse;
+ li {
+ margin: 15px 0 0 0;
+ }
+ }
+ }
+ &.toolbar {
+ &.active {
+ &>a i {
+ opacity: 0;
+ }
+ }
+ padding: 0;
+ height: $button-floating-large-size;
+ ul {
+ display: flex;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ li {
+ flex: 1;
+ display: inline-block;
+ margin: 0;
+ height: 100%;
+ transition: none;
+ a {
+ display: block;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background-color: transparent;
+ box-shadow: none;
+ color: #fff;
+ line-height: $button-floating-large-size;
+ z-index: 1;
+ i {
+ line-height: inherit;
+ }
+ }
+ }
+ }
+ }
+ position: fixed;
+ right: 23px;
+ bottom: 23px;
+ padding-top: 15px;
+ margin-bottom: 0;
+ z-index: 997;
+ ul {
+ left: 0;
+ right: 0;
+ text-align: center;
+ position: absolute;
+ bottom: 64px;
+ margin: 0;
+ visibility: hidden;
+ li {
+ margin-bottom: 15px;
+ }
+ a.btn-floating {
+ opacity: 0;
+ }
+ }
+ .fab-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ width: $button-floating-size;
+ height: $button-floating-size;
+ background-color: $button-floating-background;
+ border-radius: $button-floating-radius;
+ transform: scale(0);
+ }
+}
+
+// Flat button
+.btn-flat {
+ box-shadow: none;
+ background-color: transparent;
+ color: $button-flat-color;
+ cursor: pointer;
+ transition: background-color .2s;
+ &:focus,
+ &:hover {
+ box-shadow: none;
+ }
+ &:focus {
+ background-color: rgba(0, 0, 0, .1);
+ }
+ &.disabled,
+ &.btn-flat[disabled] {
+ background-color: transparent !important;
+ color: $button-flat-disabled-color !important;
+ cursor: default;
+ }
+}
+
+// Large button
+.btn-large {
+ @extend .btn;
+ height: $button-large-height;
+ line-height: $button-large-height;
+ font-size: $button-large-font-size;
+ padding: 0 28px;
+
+ i {
+ font-size: $button-large-icon-font-size;
+ }
+}
+
+// Small button
+.btn-small {
+ @extend .btn;
+ height: $button-small-height;
+ line-height: $button-small-height;
+ font-size: $button-small-font-size;
+ i {
+ font-size: $button-small-icon-font-size;
+ }
+}
+
+// Block button
+.btn-block {
+ display: block;
+}
diff --git a/sass/components/_cards.scss b/sass/components/_cards.scss
new file mode 100644
index 0000000..fcbf28e
--- /dev/null
+++ b/sass/components/_cards.scss
@@ -0,0 +1,195 @@
+
+
+.card-panel {
+ transition: box-shadow .25s;
+ padding: $card-padding;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ border-radius: 2px;
+ @extend .z-depth-1;
+ background-color: $card-bg-color;
+}
+
+.card {
+ position: relative;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ background-color: $card-bg-color;
+ transition: box-shadow .25s;
+ border-radius: 2px;
+ @extend .z-depth-1;
+
+
+ .card-title {
+ font-size: 24px;
+ font-weight: 300;
+ &.activator {
+ cursor: pointer;
+ }
+ }
+
+ // Card Sizes
+ &.small, &.medium, &.large {
+ position: relative;
+
+ .card-image {
+ max-height: 60%;
+ overflow: hidden;
+ }
+ .card-image + .card-content {
+ max-height: 40%;
+ }
+ .card-content {
+ max-height: 100%;
+ overflow: hidden;
+ }
+ .card-action {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ }
+
+ &.small {
+ height: 300px;
+ }
+
+ &.medium {
+ height: 400px;
+ }
+
+ &.large {
+ height: 500px;
+ }
+
+ // Horizontal Cards
+ &.horizontal {
+ &.small, &.medium, &.large {
+ .card-image {
+ height: 100%;
+ max-height: none;
+ overflow: visible;
+
+ img {
+ height: 100%;
+ }
+ }
+ }
+
+ display: flex;
+
+ .card-image {
+ max-width: 50%;
+ img {
+ border-radius: 2px 0 0 2px;
+ max-width: 100%;
+ width: auto;
+ }
+ }
+
+ .card-stacked {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ position: relative;
+
+ .card-content {
+ flex-grow: 1;
+ }
+ }
+ }
+
+ // Sticky Action Section
+ &.sticky-action {
+ .card-action {
+ z-index: 2;
+ }
+
+ .card-reveal {
+ z-index: 1;
+ padding-bottom: 64px;
+ }
+ }
+
+
+
+
+ .card-image {
+ position: relative;
+
+ // Image background for content
+ img {
+ display: block;
+ border-radius: 2px 2px 0 0;
+ position: relative;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ }
+
+ .card-title {
+ color: $card-bg-color;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ max-width: 100%;
+ padding: $card-padding;
+ }
+ }
+
+ .card-content {
+ padding: $card-padding;
+ border-radius: 0 0 2px 2px;
+
+ p {
+ margin: 0;
+ }
+ .card-title {
+ display: block;
+ line-height: 32px;
+ margin-bottom: 8px;
+
+ i {
+ line-height: 32px;
+ }
+ }
+ }
+
+ .card-action {
+ &:last-child {
+ border-radius: 0 0 2px 2px;
+ }
+ background-color: inherit; // Use inherit to inherit color classes
+ border-top: 1px solid rgba(160,160,160,.2);
+ position: relative;
+ padding: 16px $card-padding;
+
+ a:not(.btn):not(.btn-large):not(.btn-floating) {
+ color: $card-link-color;
+ margin-right: $card-padding;
+ transition: color .3s ease;
+ text-transform: uppercase;
+
+ &:hover { color: $card-link-color-light; }
+ }
+ }
+
+ .card-reveal {
+ padding: $card-padding;
+ position: absolute;
+ background-color: $card-bg-color;
+ width: 100%;
+ overflow-y: auto;
+ left: 0;
+ top: 100%;
+ height: 100%;
+ z-index: 3;
+ display: none;
+
+ .card-title {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/sass/components/_carousel.scss b/sass/components/_carousel.scss
new file mode 100644
index 0000000..0ec6730
--- /dev/null
+++ b/sass/components/_carousel.scss
@@ -0,0 +1,90 @@
+.carousel {
+ &.carousel-slider {
+ top: 0;
+ left: 0;
+
+ .carousel-fixed-item {
+ &.with-indicators {
+ bottom: 68px;
+ }
+
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 20px;
+ z-index: 1;
+ }
+
+ .carousel-item {
+ width: 100%;
+ height: 100%;
+ min-height: $carousel-height;
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ h2 {
+ font-size: 24px;
+ font-weight: 500;
+ line-height: 32px;
+ }
+
+ p {
+ font-size: 15px;
+ }
+ }
+ }
+
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+ height: $carousel-height;
+ perspective: 500px;
+ transform-style: preserve-3d;
+ transform-origin: 0% 50%;
+
+ .carousel-item {
+ visibility: hidden;
+ width: $carousel-item-width;
+ height: $carousel-item-height;
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ & > img {
+ width: 100%;
+ }
+ }
+
+ .indicators {
+ position: absolute;
+ text-align: center;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+
+ .indicator-item {
+ &.active {
+ background-color: $off-black;
+ }
+
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ height: 8px;
+ width: 8px;
+ margin: 24px 4px;
+ background-color: $inactive-color;
+
+ transition: background-color .3s;
+ border-radius: 50%;
+ }
+ }
+
+ // Materialbox compatibility
+ &.scrolling .carousel-item .materialboxed,
+ .carousel-item:not(.active) .materialboxed {
+ pointer-events: none;
+ }
+}
diff --git a/sass/components/_chips.scss b/sass/components/_chips.scss
new file mode 100644
index 0000000..27744a8
--- /dev/null
+++ b/sass/components/_chips.scss
@@ -0,0 +1,90 @@
+.chip {
+ &:focus {
+ outline: none;
+ background-color: $chip-selected-color;
+ color: #fff;
+ }
+
+ display: inline-block;
+ height: 32px;
+ font-size: 13px;
+ font-weight: 500;
+ color: rgba(0,0,0,.6);
+ line-height: 32px;
+ padding: 0 12px;
+ border-radius: 16px;
+ background-color: $chip-bg-color;
+ margin-bottom: $chip-margin;
+ margin-right: $chip-margin;
+
+ > img {
+ float: left;
+ margin: 0 8px 0 -12px;
+ height: 32px;
+ width: 32px;
+ border-radius: 50%;
+ }
+
+ .close {
+ cursor: pointer;
+ float: right;
+ font-size: 16px;
+ line-height: 32px;
+ padding-left: 8px;
+ }
+}
+
+.chips {
+ border: none;
+ border-bottom: 1px solid $chip-border-color;
+ box-shadow: none;
+ margin: $input-margin;
+ min-height: 45px;
+ outline: none;
+ transition: all .3s;
+
+ &.focus {
+ border-bottom: 1px solid $chip-selected-color;
+ box-shadow: 0 1px 0 0 $chip-selected-color;
+ }
+
+ &:hover {
+ cursor: text;
+ }
+
+ .input {
+ background: none;
+ border: 0;
+ color: rgba(0,0,0,.6);
+ display: inline-block;
+ font-size: $input-font-size;
+ height: $input-height;
+ line-height: 32px;
+ outline: 0;
+ margin: 0;
+ padding: 0 !important;
+ width: 120px !important;
+ }
+
+ .input:focus {
+ border: 0 !important;
+ box-shadow: none !important;
+ }
+
+ // Autocomplete
+ .autocomplete-content {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
+// Form prefix
+.prefix ~ .chips {
+ margin-left: 3rem;
+ width: 92%;
+ width: calc(100% - 3rem);
+}
+.chips:empty ~ label {
+ font-size: 0.8rem;
+ transform: translateY(-140%);
+}
diff --git a/sass/components/_collapsible.scss b/sass/components/_collapsible.scss
new file mode 100644
index 0000000..024324f
--- /dev/null
+++ b/sass/components/_collapsible.scss
@@ -0,0 +1,91 @@
+.collapsible {
+ border-top: 1px solid $collapsible-border-color;
+ border-right: 1px solid $collapsible-border-color;
+ border-left: 1px solid $collapsible-border-color;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ @extend .z-depth-1;
+}
+
+.collapsible-header {
+ &:focus {
+ outline: 0
+ }
+
+ display: flex;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ line-height: 1.5;
+ padding: 1rem;
+ background-color: $collapsible-header-color;
+ border-bottom: 1px solid $collapsible-border-color;
+
+ 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 $collapsible-border-color;
+ box-sizing: border-box;
+ padding: 2rem;
+}
+
+// Sidenav collapsible styling
+.sidenav,
+.sidenav.fixed {
+
+ .collapsible {
+ border: none;
+ box-shadow: none;
+
+ li { padding: 0; }
+ }
+
+ .collapsible-header {
+ background-color: transparent;
+ border: none;
+ line-height: inherit;
+ height: inherit;
+ padding: 0 $sidenav-padding;
+
+ &:hover { background-color: rgba(0,0,0,.05); }
+ i { line-height: inherit; }
+ }
+
+ .collapsible-body {
+ border: 0;
+ background-color: $collapsible-header-color;
+
+ li a {
+ padding: 0 (7.5px + $sidenav-padding)
+ 0 (15px + $sidenav-padding);
+ }
+ }
+
+}
+
+// Popout Collapsible
+
+.collapsible.popout {
+ border: none;
+ box-shadow: none;
+ > li {
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
+ // transform: scaleX(.92);
+ margin: 0 24px;
+ transition: margin .35s cubic-bezier(0.250, 0.460, 0.450, 0.940);
+ }
+ > 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;
+ // transform: scaleX(1);
+ }
+}
diff --git a/sass/components/_color-classes.scss b/sass/components/_color-classes.scss
new file mode 100644
index 0000000..155cecd
--- /dev/null
+++ b/sass/components/_color-classes.scss
@@ -0,0 +1,32 @@
+// Color Classes
+
+@each $color_name, $color in $colors {
+ @each $color_type, $color_value in $color {
+ @if $color_type == "base" {
+ .#{$color_name} {
+ background-color: $color_value !important;
+ }
+ .#{$color_name}-text {
+ color: $color_value !important;
+ }
+ }
+ @else if $color_name != "shades" {
+ .#{$color_name}.#{$color_type} {
+ background-color: $color_value !important;
+ }
+ .#{$color_name}-text.text-#{$color_type} {
+ color: $color_value !important;
+ }
+ }
+ }
+}
+
+// Shade classes
+@each $color, $color_value in $shades {
+ .#{$color} {
+ background-color: $color_value !important;
+ }
+ .#{$color}-text {
+ color: $color_value !important;
+ }
+}
diff --git a/sass/components/_color-variables.scss b/sass/components/_color-variables.scss
new file mode 100644
index 0000000..062f6a5
--- /dev/null
+++ b/sass/components/_color-variables.scss
@@ -0,0 +1,370 @@
+// Google Color Palette defined: http://www.google.com/design/spec/style/color.html
+
+$materialize-red: (
+ "base": #e51c23,
+ "lighten-5": #fdeaeb,
+ "lighten-4": #f8c1c3,
+ "lighten-3": #f3989b,
+ "lighten-2": #ee6e73,
+ "lighten-1": #ea454b,
+ "darken-1": #d0181e,
+ "darken-2": #b9151b,
+ "darken-3": #a21318,
+ "darken-4": #8b1014,
+);
+
+$red: (
+ "base": #F44336,
+ "lighten-5": #FFEBEE,
+ "lighten-4": #FFCDD2,
+ "lighten-3": #EF9A9A,
+ "lighten-2": #E57373,
+ "lighten-1": #EF5350,
+ "darken-1": #E53935,
+ "darken-2": #D32F2F,
+ "darken-3": #C62828,
+ "darken-4": #B71C1C,
+ "accent-1": #FF8A80,
+ "accent-2": #FF5252,
+ "accent-3": #FF1744,
+ "accent-4": #D50000
+);
+
+$pink: (
+ "base": #e91e63,
+ "lighten-5": #fce4ec,
+ "lighten-4": #f8bbd0,
+ "lighten-3": #f48fb1,
+ "lighten-2": #f06292,
+ "lighten-1": #ec407a,
+ "darken-1": #d81b60,
+ "darken-2": #c2185b,
+ "darken-3": #ad1457,
+ "darken-4": #880e4f,
+ "accent-1": #ff80ab,
+ "accent-2": #ff4081,
+ "accent-3": #f50057,
+ "accent-4": #c51162
+);
+
+$purple: (
+ "base": #9c27b0,
+ "lighten-5": #f3e5f5,
+ "lighten-4": #e1bee7,
+ "lighten-3": #ce93d8,
+ "lighten-2": #ba68c8,
+ "lighten-1": #ab47bc,
+ "darken-1": #8e24aa,
+ "darken-2": #7b1fa2,
+ "darken-3": #6a1b9a,
+ "darken-4": #4a148c,
+ "accent-1": #ea80fc,
+ "accent-2": #e040fb,
+ "accent-3": #d500f9,
+ "accent-4": #aa00ff
+);
+
+$deep-purple: (
+ "base": #673ab7,
+ "lighten-5": #ede7f6,
+ "lighten-4": #d1c4e9,
+ "lighten-3": #b39ddb,
+ "lighten-2": #9575cd,
+ "lighten-1": #7e57c2,
+ "darken-1": #5e35b1,
+ "darken-2": #512da8,
+ "darken-3": #4527a0,
+ "darken-4": #311b92,
+ "accent-1": #b388ff,
+ "accent-2": #7c4dff,
+ "accent-3": #651fff,
+ "accent-4": #6200ea
+);
+
+$indigo: (
+ "base": #3f51b5,
+ "lighten-5": #e8eaf6,
+ "lighten-4": #c5cae9,
+ "lighten-3": #9fa8da,
+ "lighten-2": #7986cb,
+ "lighten-1": #5c6bc0,
+ "darken-1": #3949ab,
+ "darken-2": #303f9f,
+ "darken-3": #283593,
+ "darken-4": #1a237e,
+ "accent-1": #8c9eff,
+ "accent-2": #536dfe,
+ "accent-3": #3d5afe,
+ "accent-4": #304ffe
+);
+
+$blue: (
+ "base": #2196F3,
+ "lighten-5": #E3F2FD,
+ "lighten-4": #BBDEFB,
+ "lighten-3": #90CAF9,
+ "lighten-2": #64B5F6,
+ "lighten-1": #42A5F5,
+ "darken-1": #1E88E5,
+ "darken-2": #1976D2,
+ "darken-3": #1565C0,
+ "darken-4": #0D47A1,
+ "accent-1": #82B1FF,
+ "accent-2": #448AFF,
+ "accent-3": #2979FF,
+ "accent-4": #2962FF
+);
+
+$light-blue: (
+ "base": #03a9f4,
+ "lighten-5": #e1f5fe,
+ "lighten-4": #b3e5fc,
+ "lighten-3": #81d4fa,
+ "lighten-2": #4fc3f7,
+ "lighten-1": #29b6f6,
+ "darken-1": #039be5,
+ "darken-2": #0288d1,
+ "darken-3": #0277bd,
+ "darken-4": #01579b,
+ "accent-1": #80d8ff,
+ "accent-2": #40c4ff,
+ "accent-3": #00b0ff,
+ "accent-4": #0091ea
+);
+
+$cyan: (
+ "base": #00bcd4,
+ "lighten-5": #e0f7fa,
+ "lighten-4": #b2ebf2,
+ "lighten-3": #80deea,
+ "lighten-2": #4dd0e1,
+ "lighten-1": #26c6da,
+ "darken-1": #00acc1,
+ "darken-2": #0097a7,
+ "darken-3": #00838f,
+ "darken-4": #006064,
+ "accent-1": #84ffff,
+ "accent-2": #18ffff,
+ "accent-3": #00e5ff,
+ "accent-4": #00b8d4
+);
+
+$teal: (
+ "base": #009688,
+ "lighten-5": #e0f2f1,
+ "lighten-4": #b2dfdb,
+ "lighten-3": #80cbc4,
+ "lighten-2": #4db6ac,
+ "lighten-1": #26a69a,
+ "darken-1": #00897b,
+ "darken-2": #00796b,
+ "darken-3": #00695c,
+ "darken-4": #004d40,
+ "accent-1": #a7ffeb,
+ "accent-2": #64ffda,
+ "accent-3": #1de9b6,
+ "accent-4": #00bfa5
+);
+
+$green: (
+ "base": #4CAF50,
+ "lighten-5": #E8F5E9,
+ "lighten-4": #C8E6C9,
+ "lighten-3": #A5D6A7,
+ "lighten-2": #81C784,
+ "lighten-1": #66BB6A,
+ "darken-1": #43A047,
+ "darken-2": #388E3C,
+ "darken-3": #2E7D32,
+ "darken-4": #1B5E20,
+ "accent-1": #B9F6CA,
+ "accent-2": #69F0AE,
+ "accent-3": #00E676,
+ "accent-4": #00C853
+);
+
+$light-green: (
+ "base": #8bc34a,
+ "lighten-5": #f1f8e9,
+ "lighten-4": #dcedc8,
+ "lighten-3": #c5e1a5,
+ "lighten-2": #aed581,
+ "lighten-1": #9ccc65,
+ "darken-1": #7cb342,
+ "darken-2": #689f38,
+ "darken-3": #558b2f,
+ "darken-4": #33691e,
+ "accent-1": #ccff90,
+ "accent-2": #b2ff59,
+ "accent-3": #76ff03,
+ "accent-4": #64dd17
+);
+
+$lime: (
+ "base": #cddc39,
+ "lighten-5": #f9fbe7,
+ "lighten-4": #f0f4c3,
+ "lighten-3": #e6ee9c,
+ "lighten-2": #dce775,
+ "lighten-1": #d4e157,
+ "darken-1": #c0ca33,
+ "darken-2": #afb42b,
+ "darken-3": #9e9d24,
+ "darken-4": #827717,
+ "accent-1": #f4ff81,
+ "accent-2": #eeff41,
+ "accent-3": #c6ff00,
+ "accent-4": #aeea00
+);
+
+$yellow: (
+ "base": #ffeb3b,
+ "lighten-5": #fffde7,
+ "lighten-4": #fff9c4,
+ "lighten-3": #fff59d,
+ "lighten-2": #fff176,
+ "lighten-1": #ffee58,
+ "darken-1": #fdd835,
+ "darken-2": #fbc02d,
+ "darken-3": #f9a825,
+ "darken-4": #f57f17,
+ "accent-1": #ffff8d,
+ "accent-2": #ffff00,
+ "accent-3": #ffea00,
+ "accent-4": #ffd600
+);
+
+$amber: (
+ "base": #ffc107,
+ "lighten-5": #fff8e1,
+ "lighten-4": #ffecb3,
+ "lighten-3": #ffe082,
+ "lighten-2": #ffd54f,
+ "lighten-1": #ffca28,
+ "darken-1": #ffb300,
+ "darken-2": #ffa000,
+ "darken-3": #ff8f00,
+ "darken-4": #ff6f00,
+ "accent-1": #ffe57f,
+ "accent-2": #ffd740,
+ "accent-3": #ffc400,
+ "accent-4": #ffab00
+);
+
+$orange: (
+ "base": #ff9800,
+ "lighten-5": #fff3e0,
+ "lighten-4": #ffe0b2,
+ "lighten-3": #ffcc80,
+ "lighten-2": #ffb74d,
+ "lighten-1": #ffa726,
+ "darken-1": #fb8c00,
+ "darken-2": #f57c00,
+ "darken-3": #ef6c00,
+ "darken-4": #e65100,
+ "accent-1": #ffd180,
+ "accent-2": #ffab40,
+ "accent-3": #ff9100,
+ "accent-4": #ff6d00
+);
+
+$deep-orange: (
+ "base": #ff5722,
+ "lighten-5": #fbe9e7,
+ "lighten-4": #ffccbc,
+ "lighten-3": #ffab91,
+ "lighten-2": #ff8a65,
+ "lighten-1": #ff7043,
+ "darken-1": #f4511e,
+ "darken-2": #e64a19,
+ "darken-3": #d84315,
+ "darken-4": #bf360c,
+ "accent-1": #ff9e80,
+ "accent-2": #ff6e40,
+ "accent-3": #ff3d00,
+ "accent-4": #dd2c00
+);
+
+$brown: (
+ "base": #795548,
+ "lighten-5": #efebe9,
+ "lighten-4": #d7ccc8,
+ "lighten-3": #bcaaa4,
+ "lighten-2": #a1887f,
+ "lighten-1": #8d6e63,
+ "darken-1": #6d4c41,
+ "darken-2": #5d4037,
+ "darken-3": #4e342e,
+ "darken-4": #3e2723
+);
+
+$blue-grey: (
+ "base": #607d8b,
+ "lighten-5": #eceff1,
+ "lighten-4": #cfd8dc,
+ "lighten-3": #b0bec5,
+ "lighten-2": #90a4ae,
+ "lighten-1": #78909c,
+ "darken-1": #546e7a,
+ "darken-2": #455a64,
+ "darken-3": #37474f,
+ "darken-4": #263238
+);
+
+$grey: (
+ "base": #9e9e9e,
+ "lighten-5": #fafafa,
+ "lighten-4": #f5f5f5,
+ "lighten-3": #eeeeee,
+ "lighten-2": #e0e0e0,
+ "lighten-1": #bdbdbd,
+ "darken-1": #757575,
+ "darken-2": #616161,
+ "darken-3": #424242,
+ "darken-4": #212121
+);
+
+$shades: (
+ "black": #000000,
+ "white": #FFFFFF,
+ "transparent": transparent
+);
+
+$colors: (
+ "materialize-red": $materialize-red,
+ "red": $red,
+ "pink": $pink,
+ "purple": $purple,
+ "deep-purple": $deep-purple,
+ "indigo": $indigo,
+ "blue": $blue,
+ "light-blue": $light-blue,
+ "cyan": $cyan,
+ "teal": $teal,
+ "green": $green,
+ "light-green": $light-green,
+ "lime": $lime,
+ "yellow": $yellow,
+ "amber": $amber,
+ "orange": $orange,
+ "deep-orange": $deep-orange,
+ "brown": $brown,
+ "blue-grey": $blue-grey,
+ "grey": $grey,
+ "shades": $shades
+) !default;
+
+
+// usage: color("name_of_color", "type_of_color")
+// to avoid to repeating map-get($colors, ...)
+
+@function color($color, $type) {
+ @if map-has-key($colors, $color) {
+ $curr_color: map-get($colors, $color);
+ @if map-has-key($curr_color, $type) {
+ @return map-get($curr_color, $type);
+ }
+ }
+ @warn "Unknown `#{$color}` - `#{$type}` in $colors.";
+ @return null;
+}
diff --git a/sass/components/_datepicker.scss b/sass/components/_datepicker.scss
new file mode 100644
index 0000000..d2c920b
--- /dev/null
+++ b/sass/components/_datepicker.scss
@@ -0,0 +1,191 @@
+/* Modal */
+.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;
+
+ .selects-container {
+ display: flex;
+ }
+
+ .select-wrapper {
+ input {
+ &:focus {
+ border-bottom: none;
+ }
+ border-bottom: none;
+ text-align: center;
+ margin: 0;
+ }
+
+ .caret {
+ display: none;
+ }
+ }
+
+ .select-year input {
+ width: 50px;
+ }
+
+ .select-month input {
+ width: 70px;
+ }
+}
+
+.month-prev, .month-next {
+ margin-top: 4px;
+ cursor: pointer;
+ background-color: transparent;
+ border: none;
+}
+
+
+/* Date Display */
+.datepicker-date-display {
+ flex: 1 auto;
+ background-color: $secondary-color;
+ color: #fff;
+ padding: 20px 22px;
+ font-weight: 500;
+
+ .year-text {
+ display: block;
+ font-size: 1.5rem;
+ line-height: 25px;
+ color: $datepicker-year;
+ }
+
+ .date-text {
+ display: block;
+ font-size: 2.8rem;
+ line-height: 47px;
+ font-weight: 500;
+ }
+}
+
+
+/* Calendar */
+.datepicker-calendar-container {
+ flex: 2.5 auto;
+}
+
+.datepicker-table {
+ width: 280px;
+ font-size: 1rem;
+ margin: 0 auto;
+
+ thead {
+ border-bottom: none;
+ }
+
+ th {
+ padding: 10px 5px;
+ text-align: center;
+ }
+
+ tr {
+ border: none;
+ }
+
+ abbr {
+ text-decoration: none;
+ color: $datepicker-calendar-header-color;
+ }
+
+ td {
+ &.is-today {
+ color: $secondary-color;
+ }
+
+ &.is-selected {
+ background-color: $secondary-color;
+ color: #fff;
+ }
+
+ &.is-outside-current-month,
+ &.is-disabled {
+ color: $datepicker-disabled-day-color;
+ pointer-events: none;
+ }
+
+ border-radius: 50%;
+ padding: 0;
+ }
+}
+
+.datepicker-day-button {
+ &:focus {
+ background-color: $datepicker-day-focus;
+ }
+
+ background-color: transparent;
+ border: none;
+ line-height: 38px;
+ display: block;
+ width: 100%;
+ border-radius: 50%;
+ padding: 0 5px;
+ cursor: pointer;
+ color: inherit;
+}
+
+
+/* Footer */
+.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: $secondary-color;
+ padding: 0 1rem;
+}
+
+.datepicker-clear {
+ color: $error-color;
+}
+
+
+/* Media Queries */
+@media #{$medium-and-up} {
+ .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;
+ }
+}
diff --git a/sass/components/_dropdown.scss b/sass/components/_dropdown.scss
new file mode 100644
index 0000000..0caae65
--- /dev/null
+++ b/sass/components/_dropdown.scss
@@ -0,0 +1,85 @@
+.dropdown-content {
+ &:focus {
+ outline: 0;
+ }
+
+
+ @extend .z-depth-1;
+ background-color: $dropdown-bg-color;
+ margin: 0;
+ display: none;
+ min-width: 100px;
+ overflow-y: auto;
+ opacity: 0;
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: 9999; // TODO: Check if this doesn't break other things
+ transform-origin: 0 0;
+
+
+ li {
+ &:hover, &.active {
+ background-color: $dropdown-hover-bg-color;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &.divider {
+ min-height: 0;
+ height: 1px;
+ }
+
+ & > a, & > span {
+ font-size: 16px;
+ color: $dropdown-color;
+ display: block;
+ line-height: 22px;
+ padding: (($dropdown-item-height - 22) / 2) 16px;
+ }
+
+ & > span > label {
+ top: 1px;
+ left: 0;
+ height: 18px;
+ }
+
+ // Icon alignment override
+ & > a > i {
+ height: inherit;
+ line-height: inherit;
+ float: left;
+ margin: 0 24px 0 0;
+ width: 24px;
+ }
+
+
+ clear: both;
+ color: $off-black;
+ cursor: pointer;
+ min-height: $dropdown-item-height;
+ line-height: 1.5rem;
+ width: 100%;
+ text-align: left;
+ }
+}
+
+body.keyboard-focused {
+ .dropdown-content li:focus {
+ background-color: darken($dropdown-hover-bg-color, 8%);
+ }
+}
+
+// Input field specificity bugfix
+.input-field.col .dropdown-content [type="checkbox"] + label {
+ top: 1px;
+ left: 0;
+ height: 18px;
+ transform: none;
+}
+
+.dropdown-trigger {
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/sass/components/_global.scss b/sass/components/_global.scss
new file mode 100644
index 0000000..39f33db
--- /dev/null
+++ b/sass/components/_global.scss
@@ -0,0 +1,769 @@
+//Default styles
+
+html {
+ box-sizing: border-box;
+}
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body {
+ // display: flex;
+ // min-height: 100vh;
+ // flex-direction: column;
+}
+
+main {
+ // flex: 1 0 auto;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: $font-stack;
+}
+
+ul {
+ &:not(.browser-default) {
+ padding-left: 0;
+ list-style-type: none;
+
+ & > li {
+ list-style-type: none;
+ }
+ }
+}
+
+a {
+ color: $link-color;
+ text-decoration: none;
+
+ // Gets rid of tap active state
+ -webkit-tap-highlight-color: transparent;
+}
+
+
+// Positioning
+.valign-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+
+// classic clearfix
+.clearfix {
+ clear: both;
+}
+
+
+// Z-levels
+.z-depth-0 {
+ box-shadow: none !important;
+}
+
+/* 2dp elevation modified*/
+.z-depth-1 {
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14),
+ 0 3px 1px -2px rgba(0,0,0,0.12),
+ 0 1px 5px 0 rgba(0,0,0,0.2);
+}
+.z-depth-1-half {
+ box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.14), 0 1px 7px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -1px rgba(0, 0, 0, 0.2);
+}
+
+/* 6dp elevation modified*/
+.z-depth-2 {
+ box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14),
+ 0 1px 10px 0 rgba(0,0,0,0.12),
+ 0 2px 4px -1px rgba(0,0,0,0.3);
+}
+
+/* 12dp elevation modified*/
+.z-depth-3 {
+ box-shadow: 0 8px 17px 2px rgba(0,0,0,0.14),
+ 0 3px 14px 2px rgba(0,0,0,0.12),
+ 0 5px 5px -3px rgba(0, 0, 0, 0.2);
+}
+
+/* 16dp elevation */
+.z-depth-4 {
+ box-shadow: 0 16px 24px 2px rgba(0,0,0,0.14),
+ 0 6px 30px 5px rgba(0,0,0,0.12),
+ 0 8px 10px -7px rgba(0,0,0,0.2);
+}
+
+/* 24dp elevation */
+.z-depth-5 {
+ box-shadow: 0 24px 38px 3px rgba(0,0,0,0.14),
+ 0 9px 46px 8px rgba(0,0,0,0.12),
+ 0 11px 15px -7px rgba(0,0,0,0.2);
+}
+
+.hoverable {
+ transition: box-shadow .25s;
+
+ &:hover {
+ box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+ }
+}
+
+// Dividers
+
+.divider {
+ height: 1px;
+ overflow: hidden;
+ background-color: color("grey", "lighten-2");
+}
+
+
+// Blockquote
+
+blockquote {
+ margin: 20px 0;
+ padding-left: 1.5rem;
+ border-left: 5px solid $primary-color;
+}
+
+// Icon Styles
+
+i {
+ line-height: inherit;
+
+ &.left {
+ float: left;
+ margin-right: 15px;
+ }
+ &.right {
+ float: right;
+ margin-left: 15px;
+ }
+ &.tiny {
+ font-size: 1rem;
+ }
+ &.small {
+ font-size: 2rem;
+ }
+ &.medium {
+ font-size: 4rem;
+ }
+ &.large {
+ font-size: 6rem;
+ }
+}
+
+// Images
+img.responsive-img,
+video.responsive-video {
+ max-width: 100%;
+ height: auto;
+}
+
+
+// Pagination
+
+.pagination {
+
+ li {
+ display: inline-block;
+ border-radius: 2px;
+ text-align: center;
+ vertical-align: top;
+ height: 30px;
+
+ a {
+ color: #444;
+ display: inline-block;
+ font-size: 1.2rem;
+ padding: 0 10px;
+ line-height: 30px;
+ }
+
+ &.active a { color: #fff; }
+
+ &.active { background-color: $primary-color; }
+
+ &.disabled a {
+ cursor: default;
+ color: #999;
+ }
+
+ i {
+ font-size: 2rem;
+ }
+ }
+
+
+ li.pages ul li {
+ display: inline-block;
+ float: none;
+ }
+}
+@media #{$medium-and-down} {
+ .pagination {
+ width: 100%;
+
+ li.prev,
+ li.next {
+ width: 10%;
+ }
+
+ li.pages {
+ width: 80%;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+ }
+}
+
+// Breadcrumbs
+.breadcrumb {
+ font-size: 18px;
+ color: rgba(255,255,255, .7);
+
+ i,
+ [class^="mdi-"], [class*="mdi-"],
+ i.material-icons {
+ display: inline-block;
+ float: left;
+ font-size: 24px;
+ }
+
+ &:before {
+ content: '\E5CC';
+ color: rgba(255,255,255, .7);
+ vertical-align: top;
+ display: inline-block;
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 25px;
+ margin: 0 10px 0 8px;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ &:first-child:before {
+ display: none;
+ }
+
+ &:last-child {
+ color: #fff;
+ }
+}
+
+// Parallax
+.parallax-container {
+ position: relative;
+ overflow: hidden;
+ height: 500px;
+
+ .parallax {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+
+ img {
+ opacity: 0;
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ min-width: 100%;
+ min-height: 100%;
+ transform: translate3d(0,0,0);
+ transform: translateX(-50%);
+ }
+ }
+}
+
+// Pushpin
+.pin-top, .pin-bottom {
+ position: relative;
+}
+.pinned {
+ position: fixed !important;
+}
+
+/*********************
+ Transition Classes
+**********************/
+
+ul.staggered-list li {
+ opacity: 0;
+}
+
+.fade-in {
+ opacity: 0;
+ transform-origin: 0 50%;
+}
+
+
+/*********************
+ Media Query Classes
+**********************/
+.hide-on-small-only, .hide-on-small-and-down {
+ @media #{$small-and-down} {
+ display: none !important;
+ }
+}
+.hide-on-med-and-down {
+ @media #{$medium-and-down} {
+ display: none !important;
+ }
+}
+.hide-on-med-and-up {
+ @media #{$medium-and-up} {
+ display: none !important;
+ }
+}
+.hide-on-med-only {
+ @media only screen and (min-width: $small-screen) and (max-width: $medium-screen) {
+ display: none !important;
+ }
+}
+.hide-on-large-only {
+ @media #{$large-and-up} {
+ display: none !important;
+ }
+}
+.hide-on-extra-large-only {
+ @media #{$extra-large-and-up} {
+ display: none !important;
+ }
+}
+.show-on-extra-large {
+ @media #{$extra-large-and-up} {
+ display: block !important;
+ }
+}
+.show-on-large {
+ @media #{$large-and-up} {
+ display: block !important;
+ }
+}
+.show-on-medium {
+ @media only screen and (min-width: $small-screen) and (max-width: $medium-screen) {
+ display: block !important;
+ }
+}
+.show-on-small {
+ @media #{$small-and-down} {
+ display: block !important;
+ }
+}
+.show-on-medium-and-up {
+ @media #{$medium-and-up} {
+ display: block !important;
+ }
+}
+.show-on-medium-and-down {
+ @media #{$medium-and-down} {
+ display: block !important;
+ }
+}
+
+
+// Center text on mobile
+.center-on-small-only {
+ @media #{$small-and-down} {
+ text-align: center;
+ }
+}
+
+// Footer
+.page-footer {
+ padding-top: 20px;
+ color: $footer-font-color;
+ background-color: $footer-bg-color;
+
+ .footer-copyright {
+ overflow: hidden;
+ min-height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 0px;
+ color: $footer-copyright-font-color;
+ background-color: $footer-copyright-bg-color;
+ }
+}
+
+// Tables
+table, th, td {
+ border: none;
+}
+
+table {
+ width:100%;
+ display: table;
+ border-collapse: collapse;
+ border-spacing: 0;
+
+ &.striped {
+ tr {
+ border-bottom: none;
+ }
+
+ > tbody {
+ > tr:nth-child(odd) {
+ background-color: $table-striped-color;
+ }
+
+ > tr > td {
+ border-radius: 0;
+ }
+ }
+ }
+
+ &.highlight > tbody > tr {
+ transition: background-color .25s ease;
+ &:hover {
+ background-color: $table-striped-color;
+ }
+ }
+
+ &.centered {
+ thead tr th, tbody tr td {
+ text-align: center;
+ }
+ }
+}
+
+tr {
+ border-bottom: 1px solid $table-border-color;
+}
+
+td, th{
+ padding: 15px 5px;
+ display: table-cell;
+ text-align: left;
+ vertical-align: middle;
+ border-radius: 2px;
+}
+
+// Responsive Table
+@media #{$medium-and-down} {
+
+ table.responsive-table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ display: block;
+ position: relative;
+
+ td:empty:before {
+ content: '\00a0';
+ }
+
+ th,
+ td {
+ margin: 0;
+ vertical-align: top;
+ }
+
+ th { text-align: left; }
+ thead {
+ display: block;
+ float: left;
+
+ tr {
+ display: block;
+ padding: 0 10px 0 0;
+
+ th::before {
+ content: "\00a0";
+ }
+ }
+ }
+ tbody {
+ display: block;
+ width: auto;
+ position: relative;
+ overflow-x: auto;
+ white-space: nowrap;
+
+ tr {
+ display: inline-block;
+ vertical-align: top;
+ }
+ }
+ th {
+ display: block;
+ text-align: right;
+ }
+ td {
+ display: block;
+ min-height: 1.25em;
+ text-align: left;
+ }
+ tr {
+ border-bottom: none;
+ padding: 0 10px;
+ }
+
+ /* sort out borders */
+ thead {
+ border: 0;
+ border-right: 1px solid $table-border-color;
+ }
+ }
+
+}
+
+
+// Collections
+.collection {
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ border: 1px solid $collection-border-color;
+ border-radius: 2px;
+ overflow: hidden;
+ position: relative;
+
+ .collection-item {
+ background-color: $collection-bg-color;
+ line-height: $collection-line-height;
+ padding: 10px 20px;
+ margin: 0;
+ border-bottom: 1px solid $collection-border-color;
+
+ // Avatar Collection
+ &.avatar {
+ min-height: 84px;
+ padding-left: 72px;
+ position: relative;
+
+ // Don't style circles inside preloader classes.
+ &:not(.circle-clipper) > .circle,
+ :not(.circle-clipper) > .circle {
+ position: absolute;
+ width: 42px;
+ height: 42px;
+ overflow: hidden;
+ left: 15px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ i.circle {
+ font-size: 18px;
+ line-height: 42px;
+ color: #fff;
+ background-color: #999;
+ text-align: center;
+ }
+
+
+ .title {
+ font-size: 16px;
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .secondary-content {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ }
+
+ }
+
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &.active {
+ background-color: $collection-active-bg-color;
+ color: $collection-active-color;
+
+ .secondary-content {
+ color: #fff;
+ }
+ }
+ }
+ a.collection-item{
+ display: block;
+ transition: .25s;
+ color: $collection-link-color;
+ &:not(.active) {
+ &:hover {
+ background-color: $collection-hover-bg-color;
+ }
+ }
+ }
+
+ &.with-header {
+ .collection-header {
+ background-color: $collection-bg-color;
+ border-bottom: 1px solid $collection-border-color;
+ padding: 10px 20px;
+ }
+ .collection-item {
+ padding-left: 30px;
+ }
+ .collection-item.avatar {
+ padding-left: 72px;
+ }
+ }
+
+}
+// Made less specific to allow easier overriding
+.secondary-content {
+ float: right;
+ color: $secondary-color;
+}
+.collapsible .collection {
+ margin: 0;
+ border: none;
+}
+
+
+
+// Responsive Videos
+.video-container {
+ position: relative;
+ padding-bottom: 56.25%;
+ height: 0;
+ overflow: hidden;
+
+ iframe, object, embed {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+}
+
+// Progress Bar
+.progress {
+ position: relative;
+ height: 4px;
+ display: block;
+ width: 100%;
+ background-color: lighten($progress-bar-color, 40%);
+ border-radius: 2px;
+ margin: $element-top-margin 0 $element-bottom-margin 0;
+ overflow: hidden;
+ .determinate {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ background-color: $progress-bar-color;
+ transition: width .3s linear;
+ }
+ .indeterminate {
+ background-color: $progress-bar-color;
+ &:before {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left:0;
+ bottom: 0;
+ will-change: left, right;
+ // Custom bezier
+ animation: indeterminate 2.1s cubic-bezier(0.650, 0.815, 0.735, 0.395) infinite;
+
+ }
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left:0;
+ bottom: 0;
+ will-change: left, right;
+ // Custom bezier
+ animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.840, 0.440, 1.000) infinite;
+ animation-delay: 1.15s;
+ }
+ }
+}
+@keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right:100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+}
+
+@keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+}
+
+
+/*******************
+ Utility Classes
+*******************/
+
+.hide {
+ display: none !important;
+}
+
+// Text Align
+.left-align {
+ text-align: left;
+}
+.right-align {
+ text-align: right
+}
+.center, .center-align {
+ text-align: center;
+}
+
+.left {
+ float: left !important;
+}
+.right {
+ float: right !important;
+}
+
+// No Text Select
+.no-select {
+ user-select: none;
+}
+
+.circle {
+ border-radius: 50%;
+}
+
+.center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.truncate {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.no-padding {
+ padding: 0 !important;
+}
diff --git a/sass/components/_grid.scss b/sass/components/_grid.scss
new file mode 100644
index 0000000..8892f05
--- /dev/null
+++ b/sass/components/_grid.scss
@@ -0,0 +1,156 @@
+.container {
+ margin: 0 auto;
+ max-width: 1280px;
+ width: 90%;
+}
+@media #{$medium-and-up} {
+ .container {
+ width: 85%;
+ }
+}
+@media #{$large-and-up} {
+ .container {
+ width: 70%;
+ }
+}
+.col .row {
+ margin-left: (-1 * $gutter-width / 2);
+ margin-right: (-1 * $gutter-width / 2);
+}
+
+.section {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+
+ &.no-pad {
+ padding: 0;
+ }
+ &.no-pad-bot {
+ padding-bottom: 0;
+ }
+ &.no-pad-top {
+ padding-top: 0;
+ }
+}
+
+
+// Mixins to eliminate code repitition
+@mixin reset-offset {
+ margin-left: auto;
+ left: auto;
+ right: auto;
+}
+@mixin grid-classes($size, $i, $perc) {
+ &.offset-#{$size}#{$i} {
+ margin-left: $perc;
+ }
+ &.pull-#{$size}#{$i} {
+ right: $perc;
+ }
+ &.push-#{$size}#{$i} {
+ left: $perc;
+ }
+}
+
+
+.row {
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 20px;
+
+ // Clear floating children
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+
+ .col {
+ float: left;
+ box-sizing: border-box;
+ padding: 0 $gutter-width / 2;
+ min-height: 1px;
+
+ &[class*="push-"],
+ &[class*="pull-"] {
+ position: relative;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.s#{$i} {
+ width: $perc;
+ @include reset-offset;
+ }
+ $i: $i + 1;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ @include grid-classes("s", $i, $perc);
+ $i: $i + 1;
+ }
+
+ @media #{$medium-and-up} {
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.m#{$i} {
+ width: $perc;
+ @include reset-offset;
+ }
+ $i: $i + 1
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ @include grid-classes("m", $i, $perc);
+ $i: $i + 1;
+ }
+ }
+
+ @media #{$large-and-up} {
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.l#{$i} {
+ width: $perc;
+ @include reset-offset;
+ }
+ $i: $i + 1;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ @include grid-classes("l", $i, $perc);
+ $i: $i + 1;
+ }
+ }
+
+ @media #{$extra-large-and-up} {
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ &.xl#{$i} {
+ width: $perc;
+ @include reset-offset;
+ }
+ $i: $i + 1;
+ }
+
+ $i: 1;
+ @while $i <= $num-cols {
+ $perc: unquote((100 / ($num-cols / $i)) + "%");
+ @include grid-classes("xl", $i, $perc);
+ $i: $i + 1;
+ }
+ }
+ }
+}
diff --git a/sass/components/_icons-material-design.scss b/sass/components/_icons-material-design.scss
new file mode 100644
index 0000000..2aa6a4a
--- /dev/null
+++ b/sass/components/_icons-material-design.scss
@@ -0,0 +1,5 @@
+/* This is needed for some mobile phones to display the Google Icon font properly */
+.material-icons {
+ text-rendering: optimizeLegibility;
+ font-feature-settings: 'liga';
+}
diff --git a/sass/components/_materialbox.scss b/sass/components/_materialbox.scss
new file mode 100644
index 0000000..3027667
--- /dev/null
+++ b/sass/components/_materialbox.scss
@@ -0,0 +1,43 @@
+.materialboxed {
+ &:hover {
+ &:not(.active) {
+ opacity: .8;
+ }
+ }
+
+ display: block;
+ cursor: zoom-in;
+ position: relative;
+ transition: opacity .4s;
+ -webkit-backface-visibility: hidden;
+
+ &.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;
+} \ No newline at end of file
diff --git a/sass/components/_modal.scss b/sass/components/_modal.scss
new file mode 100644
index 0000000..38cf3ce
--- /dev/null
+++ b/sass/components/_modal.scss
@@ -0,0 +1,94 @@
+.modal {
+ &:focus {
+ outline: none;
+ }
+
+ @extend .z-depth-5;
+
+ 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;
+
+ @media #{$medium-and-down} {
+ width: 80%;
+ }
+
+ h1,h2,h3,h4 {
+ margin-top: 0;
+ }
+
+ .modal-content {
+ padding: 24px;
+ }
+ .modal-close {
+ cursor: pointer;
+ }
+
+ .modal-footer {
+ border-radius: 0 0 2px 2px;
+ background-color: #fafafa;
+ padding: 4px 6px;
+ height: 56px;
+ width: 100%;
+ text-align: right;
+
+ .btn, .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 with fixed action footer
+.modal.modal-fixed-footer {
+ padding: 0;
+ height: 70%;
+
+ .modal-content {
+ position: absolute;
+ height: calc(100% - 56px);
+ max-height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ }
+
+ .modal-footer {
+ border-top: 1px solid rgba(0,0,0,.1);
+ position: absolute;
+ bottom: 0;
+ }
+}
+
+// Modal Bottom Sheet Style
+.modal.bottom-sheet {
+ top: auto;
+ bottom: -100%;
+ margin: 0;
+ width: 100%;
+ max-height: 45%;
+ border-radius: 0;
+ will-change: bottom, opacity;
+}
diff --git a/sass/components/_navbar.scss b/sass/components/_navbar.scss
new file mode 100644
index 0000000..0317bb2
--- /dev/null
+++ b/sass/components/_navbar.scss
@@ -0,0 +1,208 @@
+nav {
+ &.nav-extended {
+ height: auto;
+
+ .nav-wrapper {
+ min-height: $navbar-height-mobile;
+ height: auto;
+ }
+
+ .nav-content {
+ position: relative;
+ line-height: normal;
+ }
+ }
+
+ color: $navbar-font-color;
+ @extend .z-depth-1;
+ background-color: $primary-color;
+ width: 100%;
+ height: $navbar-height-mobile;
+ line-height: $navbar-line-height-mobile;
+
+ a { color: $navbar-font-color; }
+
+ i,
+ [class^="mdi-"], [class*="mdi-"],
+ i.material-icons {
+ display: block;
+ font-size: 24px;
+ height: $navbar-height-mobile;
+ line-height: $navbar-line-height-mobile;
+ }
+
+ .nav-wrapper {
+ position: relative;
+ height: 100%;
+ }
+
+ @media #{$large-and-up} {
+ a.sidenav-trigger { display: none; }
+ }
+
+
+ // Collapse button
+ .sidenav-trigger {
+ float: left;
+ position: relative;
+ z-index: 1;
+ height: $navbar-height-mobile;
+ margin: 0 18px;
+
+ i {
+ height: $navbar-height-mobile;
+ line-height: $navbar-line-height-mobile;
+ }
+ }
+
+
+ // Logo
+ .brand-logo {
+ position: absolute;
+ color: $navbar-font-color;
+ display: inline-block;
+ font-size: $navbar-brand-font-size;
+ padding: 0;
+
+ &.center {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ @media #{$medium-and-down} {
+ left: 50%;
+ transform: translateX(-50%);
+
+ &.left, &.right {
+ padding: 0;
+ transform: none;
+ }
+
+ &.left { left: 0.5rem; }
+ &.right {
+ right: 0.5rem;
+ left: auto;
+ }
+ }
+
+ &.right {
+ right: 0.5rem;
+ padding: 0;
+ }
+
+ i,
+ [class^="mdi-"], [class*="mdi-"],
+ i.material-icons {
+ float: left;
+ margin-right: 15px;
+ }
+ }
+
+
+ // Title
+ .nav-title {
+ display: inline-block;
+ font-size: 32px;
+ padding: 28px 0;
+ }
+
+
+ // Navbar Links
+ ul {
+ margin: 0;
+
+ li {
+ transition: background-color .3s;
+ float: left;
+ padding: 0;
+
+ &.active {
+ background-color: rgba(0,0,0,.1);
+ }
+ }
+ a {
+ transition: background-color .3s;
+ font-size: $navbar-font-size;
+ color: $navbar-font-color;
+ display: block;
+ padding: 0 15px;
+ cursor: pointer;
+
+ &.btn, &.btn-large, &.btn-flat, &.btn-floating {
+ margin-top: -2px;
+ margin-left: 15px;
+ margin-right: 15px;
+
+ & > .material-icons {
+ height: inherit;
+ line-height: inherit;
+ }
+ }
+
+ &:hover {
+ background-color: rgba(0,0,0,.1);
+ }
+ }
+
+ &.left {
+ float: left;
+ }
+ }
+
+ // Navbar Search Form
+ form {
+ height: 100%;
+ }
+
+ .input-field {
+ margin: 0;
+ height: 100%;
+
+ input {
+ height: 100%;
+ font-size: 1.2rem;
+ border: none;
+ padding-left: 2rem;
+
+ &:focus, &[type=text]:valid, &[type=password]:valid,
+ &[type=email]:valid, &[type=url]:valid, &[type=date]:valid {
+ border: none;
+ box-shadow: none;
+ }
+ }
+
+ label {
+ top: 0;
+ left: 0;
+
+ i {
+ color: rgba(255,255,255,.7);
+ transition: color .3s;
+ }
+ &.active i { color: $navbar-font-color; }
+ }
+ }
+}
+
+// Fixed Navbar
+.navbar-fixed {
+ position: relative;
+ height: $navbar-height-mobile;
+ z-index: 997;
+
+ nav {
+ position: fixed;
+ }
+}
+@media #{$medium-and-up} {
+ nav.nav-extended .nav-wrapper {
+ min-height: $navbar-height;
+ }
+ nav, nav .nav-wrapper i, nav a.sidenav-trigger, nav a.sidenav-trigger i {
+ height: $navbar-height;
+ line-height: $navbar-line-height;
+ }
+ .navbar-fixed {
+ height: $navbar-height;
+ }
+}
diff --git a/sass/components/_normalize.scss b/sass/components/_normalize.scss
new file mode 100644
index 0000000..fa4e73d
--- /dev/null
+++ b/sass/components/_normalize.scss
@@ -0,0 +1,447 @@
+/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in
+ * IE on Windows Phone and in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers (opinionated).
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+article,
+aside,
+footer,
+header,
+nav,
+section {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in IE.
+ */
+
+figcaption,
+figure,
+main { /* 1 */
+ display: block;
+}
+
+/**
+ * Add the correct margin in IE 8.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * 1. Remove the gray background on active links in IE 10.
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
+ */
+
+a {
+ background-color: transparent; /* 1 */
+ -webkit-text-decoration-skip: objects; /* 2 */
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57- and Firefox 39-.
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
+ */
+
+b,
+strong {
+ font-weight: inherit;
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font style in Android 4.3-.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Add the correct background and color in IE 9-.
+ */
+
+mark {
+ background-color: #ff0;
+ color: #000;
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+audio,
+video {
+ display: inline-block;
+}
+
+/**
+ * Add the correct display in iOS 4-7.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Remove the border on images inside links in IE 10-.
+ */
+
+img {
+ border-style: none;
+}
+
+/**
+ * Hide the overflow in IE.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers (opinionated).
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: sans-serif; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+ * controls in Android 4.
+ * 2. Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+html [type="button"], /* 1 */
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * 1. Add the correct display in IE 9-.
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Remove the default vertical scrollbar in IE.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10-.
+ * 2. Remove the padding in IE 10-.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in Edge, IE, and Firefox.
+ */
+
+details, /* 1 */
+menu {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Scripting
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+canvas {
+ display: inline-block;
+}
+
+/**
+ * Add the correct display in IE.
+ */
+
+template {
+ display: none;
+}
+
+/* Hidden
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10-.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/sass/components/_preloader.scss b/sass/components/_preloader.scss
new file mode 100644
index 0000000..cfe2993
--- /dev/null
+++ b/sass/components/_preloader.scss
@@ -0,0 +1,334 @@
+/*
+ @license
+ Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
+ This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
+ The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
+ The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
+ Code distributed by Google as part of the polymer project is also
+ subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
+ */
+
+/**************************/
+/* STYLES FOR THE SPINNER */
+/**************************/
+
+/*
+ * Constants:
+ * STROKEWIDTH = 3px
+ * ARCSIZE = 270 degrees (amount of circle the arc takes up)
+ * ARCTIME = 1333ms (time it takes to expand and contract arc)
+ * ARCSTARTROT = 216 degrees (how much the start location of the arc
+ * should rotate each time, 216 gives us a
+ * 5 pointed star shape (it's 360/5 * 3).
+ * For a 7 pointed star, we might do
+ * 360/7 * 3 = 154.286)
+ * CONTAINERWIDTH = 28px
+ * SHRINK_TIME = 400ms
+ */
+
+
+.preloader-wrapper {
+ display: inline-block;
+ position: relative;
+ width: 50px;
+ height: 50px;
+
+ &.small {
+ width: 36px;
+ height: 36px;
+ }
+
+ &.big {
+ width: 64px;
+ height: 64px;
+ }
+
+ &.active {
+ /* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
+ -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: $spinner-default-color;
+}
+
+.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;
+}
+
+/**
+ * IMPORTANT NOTE ABOUT CSS ANIMATION PROPERTIES (keanulee):
+ *
+ * iOS Safari (tested on iOS 8.1) does not handle animation-delay very well - it doesn't
+ * guarantee that the animation will start _exactly_ after that value. So we avoid using
+ * animation-delay and instead set custom keyframes for each color (as redundant as it
+ * seems).
+ *
+ * We write out each animation in full (instead of separating animation-name,
+ * animation-duration, etc.) because under the polyfill, Safari does not recognize those
+ * specific properties properly, treats them as -webkit-animation, and overrides the
+ * other animation rules. See https://github.com/Polymer/platform/issues/53.
+ */
+.active .spinner-layer.spinner-blue {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, blue-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-red {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, red-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-yellow {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, yellow-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .spinner-layer.spinner-green {
+ /* durations: 4 * ARCTIME */
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both, green-fade-in-out 5332ms cubic-bezier(0.4, 0.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 {
+ /* durations: 4 * ARCTIME */
+ opacity: 1;
+ -webkit-animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+@-webkit-keyframes fill-unfill-rotate {
+ 12.5% { -webkit-transform: rotate(135deg); } /* 0.5 * ARCSIZE */
+ 25% { -webkit-transform: rotate(270deg); } /* 1 * ARCSIZE */
+ 37.5% { -webkit-transform: rotate(405deg); } /* 1.5 * ARCSIZE */
+ 50% { -webkit-transform: rotate(540deg); } /* 2 * ARCSIZE */
+ 62.5% { -webkit-transform: rotate(675deg); } /* 2.5 * ARCSIZE */
+ 75% { -webkit-transform: rotate(810deg); } /* 3 * ARCSIZE */
+ 87.5% { -webkit-transform: rotate(945deg); } /* 3.5 * ARCSIZE */
+ to { -webkit-transform: rotate(1080deg); } /* 4 * ARCSIZE */
+}
+
+@keyframes fill-unfill-rotate {
+ 12.5% { transform: rotate(135deg); } /* 0.5 * ARCSIZE */
+ 25% { transform: rotate(270deg); } /* 1 * ARCSIZE */
+ 37.5% { transform: rotate(405deg); } /* 1.5 * ARCSIZE */
+ 50% { transform: rotate(540deg); } /* 2 * ARCSIZE */
+ 62.5% { transform: rotate(675deg); } /* 2.5 * ARCSIZE */
+ 75% { transform: rotate(810deg); } /* 3 * ARCSIZE */
+ 87.5% { transform: rotate(945deg); } /* 3.5 * ARCSIZE */
+ to { transform: rotate(1080deg); } /* 4 * ARCSIZE */
+}
+
+@-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; }
+}
+
+/**
+ * Patch the gap that appear between the two adjacent div.circle-clipper while the
+ * spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11).
+ */
+.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 {
+ width: 200%;
+ height: 100%;
+ border-width: 3px; /* STROKEWIDTH */
+ 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;
+ }
+
+ &.left .circle {
+ left: 0;
+ border-right-color: transparent !important;
+ -webkit-transform: rotate(129deg);
+ transform: rotate(129deg);
+ }
+ &.right .circle {
+ left: -100%;
+ border-left-color: transparent !important;
+ -webkit-transform: rotate(-129deg);
+ transform: rotate(-129deg);
+ }
+}
+
+
+
+.active .circle-clipper.left .circle {
+ /* duration: ARCTIME */
+ -webkit-animation: left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+}
+
+.active .circle-clipper.right .circle {
+ /* duration: ARCTIME */
+ -webkit-animation: right-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
+ animation: right-spin 1333ms cubic-bezier(0.4, 0.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 {
+ /* duration: SHRINK_TIME */
+ -webkit-animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1);
+ animation: container-rotate 1568ms linear infinite, fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1);
+}
+
+@-webkit-keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+@keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
diff --git a/sass/components/_pulse.scss b/sass/components/_pulse.scss
new file mode 100644
index 0000000..a690f36
--- /dev/null
+++ b/sass/components/_pulse.scss
@@ -0,0 +1,34 @@
+.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;
+ }
+
+ overflow: visible;
+ position: relative;
+}
+
+@keyframes pulse-animation {
+ 0% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 0;
+ transform: scale(1.5);
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(1.5);
+ }
+}
diff --git a/sass/components/_sidenav.scss b/sass/components/_sidenav.scss
new file mode 100644
index 0000000..98a71d8
--- /dev/null
+++ b/sass/components/_sidenav.scss
@@ -0,0 +1,216 @@
+.sidenav {
+ position: fixed;
+ width: $sidenav-width;
+ left: 0;
+ top: 0;
+ margin: 0;
+ transform: translateX(-100%);
+ height: 100%;
+ height: calc(100% + 60px);
+ height: -moz-calc(100%); //Temporary Firefox Fix
+ padding-bottom: 60px;
+ background-color: $sidenav-bg-color;
+ z-index: 999;
+ overflow-y: auto;
+ will-change: transform;
+ backface-visibility: hidden;
+ transform: translateX(-105%);
+
+ @extend .z-depth-1;
+
+ // Right Align
+ &.right-aligned {
+ right: 0;
+ transform: translateX(105%);
+ left: auto;
+ transform: translateX(100%);
+ }
+
+ .collapsible {
+ margin: 0;
+ }
+
+
+ li {
+ float: none;
+ line-height: $sidenav-line-height;
+
+ &.active { background-color: rgba(0,0,0,.05); }
+ }
+
+ li > a {
+ color: $sidenav-font-color;
+ display: block;
+ font-size: $sidenav-font-size;
+ font-weight: 500;
+ height: $sidenav-item-height;
+ line-height: $sidenav-line-height;
+ padding: 0 ($sidenav-padding * 2);
+
+ &:hover { background-color: rgba(0,0,0,.05);}
+
+ &.btn, &.btn-large, &.btn-flat, &.btn-floating {
+ margin: 10px 15px;
+ }
+
+ &.btn,
+ &.btn-large,
+ &.btn-floating { color: $button-raised-color; }
+ &.btn-flat { color: $button-flat-color; }
+
+ &.btn:hover,
+ &.btn-large:hover { background-color: lighten($button-raised-background, 5%); }
+ &.btn-floating:hover { background-color: $button-raised-background; }
+
+ & > i,
+ & > [class^="mdi-"], li > a > [class*="mdi-"],
+ & > i.material-icons {
+ float: left;
+ height: $sidenav-item-height;
+ line-height: $sidenav-line-height;
+ margin: 0 ($sidenav-padding * 2) 0 0;
+ width: $sidenav-item-height / 2;
+ color: rgba(0,0,0,.54);
+ }
+ }
+
+
+ .divider {
+ margin: ($sidenav-padding / 2) 0 0 0;
+ }
+
+ .subheader {
+ &:hover {
+ background-color: transparent;
+ }
+
+ cursor: initial;
+ pointer-events: none;
+ color: rgba(0,0,0,.54);
+ font-size: $sidenav-font-size;
+ font-weight: 500;
+ line-height: $sidenav-line-height;
+ }
+
+ .user-view {
+ position: relative;
+ padding: ($sidenav-padding * 2) ($sidenav-padding * 2) 0;
+ margin-bottom: $sidenav-padding / 2;
+
+ & > a {
+ &:hover { background-color: transparent; }
+ height: auto;
+ padding: 0;
+ }
+
+ .background {
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: -1;
+ }
+
+ .circle, .name, .email {
+ display: block;
+ }
+
+ .circle {
+ height: 64px;
+ width: 64px;
+ }
+
+ .name,
+ .email {
+ font-size: $sidenav-font-size;
+ line-height: $sidenav-line-height / 2;
+ }
+
+ .name {
+ margin-top: 16px;
+ font-weight: 500;
+ }
+
+ .email {
+ padding-bottom: 16px;
+ font-weight: 400;
+ }
+ }
+}
+
+
+// Touch interaction
+.drag-target {
+ // Right Align
+ &.right-aligned {
+ right: 0;
+ }
+
+ height: 100%;
+ width: 10px;
+ position: fixed;
+ top: 0;
+ z-index: 998;
+}
+
+
+// Fixed Sidenav shown
+.sidenav.sidenav-fixed {
+ // Right Align
+ &.right-aligned {
+ right: 0;
+ left: auto;
+ }
+
+ left: 0;
+ transform: translateX(0);
+ position: fixed;
+}
+
+// Fixed Sidenav hide on smaller
+@media #{$medium-and-down} {
+ .sidenav {
+ &.sidenav-fixed {
+ transform: translateX(-105%);
+
+ &.right-aligned {
+ transform: translateX(105%);
+ }
+ }
+
+ > a {
+ padding: 0 $sidenav-padding;
+ }
+
+ .user-view {
+ padding: $sidenav-padding $sidenav-padding 0;
+ }
+ }
+}
+
+
+.sidenav .collapsible-body > ul:not(.collapsible) > li.active,
+.sidenav.sidenav-fixed .collapsible-body > ul:not(.collapsible) > li.active {
+ background-color: $primary-color;
+ a {
+ color: $sidenav-bg-color;
+ }
+}
+.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,.5);
+ z-index: 997;
+ display: none;
+}
diff --git a/sass/components/_slider.scss b/sass/components/_slider.scss
new file mode 100644
index 0000000..5d7c27e
--- /dev/null
+++ b/sass/components/_slider.scss
@@ -0,0 +1,92 @@
+.slider {
+ position: relative;
+ height: 400px;
+ width: 100%;
+
+ // Fullscreen slider
+ &.fullscreen {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ ul.slides {
+ height: 100%;
+ }
+
+ ul.indicators {
+ z-index: 2;
+ bottom: 30px;
+ }
+ }
+
+ .slides {
+ background-color: $slider-bg-color;
+ margin: 0;
+ height: 400px;
+
+ li {
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: inherit;
+ overflow: hidden;
+
+ img {
+ height: 100%;
+ width: 100%;
+ background-size: cover;
+ background-position: center;
+ }
+
+ .caption {
+ color: #fff;
+ position: absolute;
+ top: 15%;
+ left: 15%;
+ width: 70%;
+ opacity: 0;
+
+ p { color: $slider-bg-color-light; }
+ }
+
+ &.active {
+ z-index: 2;
+ }
+ }
+ }
+
+
+ .indicators {
+ position: absolute;
+ text-align: center;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+
+ .indicator-item {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ height: 16px;
+ width: 16px;
+ margin: 0 12px;
+ background-color: $slider-bg-color-light;
+
+ transition: background-color .3s;
+ border-radius: 50%;
+
+ &.active {
+ background-color: $slider-indicator-color;
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/sass/components/_table_of_contents.scss b/sass/components/_table_of_contents.scss
new file mode 100644
index 0000000..638009d
--- /dev/null
+++ b/sass/components/_table_of_contents.scss
@@ -0,0 +1,33 @@
+/***************
+ Nav List
+***************/
+.table-of-contents {
+ &.fixed {
+ position: fixed;
+ }
+
+ li {
+ padding: 2px 0;
+ }
+ 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;
+
+ &:hover {
+ color: lighten(#757575, 20%);
+ padding-left: 15px;
+ border-left: 1px solid $primary-color;
+ }
+ &.active {
+ font-weight: 500;
+ padding-left: 14px;
+ border-left: 2px solid $primary-color;
+ }
+ }
+}
diff --git a/sass/components/_tabs.scss b/sass/components/_tabs.scss
new file mode 100644
index 0000000..072d4b6
--- /dev/null
+++ b/sass/components/_tabs.scss
@@ -0,0 +1,99 @@
+.tabs {
+ &.tabs-transparent {
+ background-color: transparent;
+
+ .tab a,
+ .tab.disabled a,
+ .tab.disabled a:hover {
+ color: rgba(255,255,255,0.7);
+ }
+
+ .tab a:hover,
+ .tab a.active {
+ color: #fff;
+ }
+
+ .indicator {
+ background-color: #fff;
+ }
+ }
+
+ &.tabs-fixed-width {
+ display: flex;
+
+ .tab {
+ flex-grow: 1;
+ }
+ }
+
+ position: relative;
+ overflow-x: auto;
+ overflow-y: hidden;
+ height: 48px;
+ width: 100%;
+ background-color: $tabs-bg-color;
+ margin: 0 auto;
+ white-space: nowrap;
+
+ .tab {
+ display: inline-block;
+ text-align: center;
+ line-height: 48px;
+ height: 48px;
+ padding: 0;
+ margin: 0;
+ text-transform: uppercase;
+
+ a {
+ &:focus,
+ &:focus.active {
+ background-color: transparentize($tabs-underline-color, .8);
+ outline: none;
+ }
+
+ &:hover,
+ &.active {
+ background-color: transparent;
+ color: $tabs-text-color;
+ }
+
+ color: rgba($tabs-text-color, .7);
+ display: block;
+ width: 100%;
+ height: 100%;
+ padding: 0 24px;
+ font-size: 14px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ transition: color .28s ease, background-color .28s ease;
+ }
+
+ &.disabled a,
+ &.disabled a:hover {
+ color: rgba($tabs-text-color, .4);
+ cursor: default;
+ }
+ }
+ .indicator {
+ position: absolute;
+ bottom: 0;
+ height: 2px;
+ background-color: $tabs-underline-color;
+ will-change: left, right;
+ }
+}
+
+// Fixed Sidenav hide on smaller
+@media #{$medium-and-down} {
+ .tabs {
+ display: flex;
+
+ .tab {
+ flex-grow: 1;
+
+ a {
+ padding: 0 12px;
+ }
+ }
+ }
+}
diff --git a/sass/components/_tapTarget.scss b/sass/components/_tapTarget.scss
new file mode 100644
index 0000000..49aecd5
--- /dev/null
+++ b/sass/components/_tapTarget.scss
@@ -0,0 +1,103 @@
+.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 {
+ transform: scale(1);
+ opacity: .95;
+ transition:
+ transform .3s cubic-bezier(.42,0,.58,1),
+ opacity .3s cubic-bezier(.42,0,.58,1);
+ }
+
+ .tap-target-wave::before {
+ transform: scale(1);
+ }
+ .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: $primary-color;
+ 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 .3s cubic-bezier(.42,0,.58,1),
+ opacity .3s cubic-bezier(.42,0,.58,1);
+}
+
+.tap-target-content {
+ position: relative;
+ display: table-cell;
+}
+
+.tap-target-wave {
+ &::before,
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background-color: #ffffff;
+ }
+ &::before {
+ transform: scale(0);
+ transition: transform .3s;
+ }
+ &::after {
+ visibility: hidden;
+ transition:
+ opacity .3s,
+ transform .3s,
+ visibility 0s;
+ z-index: -1;
+ }
+
+ position: absolute;
+ border-radius: 50%;
+ z-index: 10001;
+}
+
+.tap-target-origin {
+ &:not(.btn),
+ &:not(.btn):hover {
+ background: none;
+ }
+
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+
+ z-index: 10002;
+ position: absolute !important;
+}
+
+@media only screen and (max-width: 600px) {
+ .tap-target, .tap-target-wrapper {
+ width: 600px;
+ height: 600px;
+ }
+}
diff --git a/sass/components/_timepicker.scss b/sass/components/_timepicker.scss
new file mode 100644
index 0000000..fa602fb
--- /dev/null
+++ b/sass/components/_timepicker.scss
@@ -0,0 +1,183 @@
+/* Timepicker Containers */
+.timepicker-modal {
+ max-width: 325px;
+ max-height: none;
+}
+
+.timepicker-container.modal-content {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+}
+
+.text-primary {
+ color: rgba(255, 255, 255, 1);
+}
+
+
+/* Clock Digital Display */
+.timepicker-digital-display {
+ flex: 1 auto;
+ background-color: $secondary-color;
+ 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;
+}
+
+
+/* Analog Clock Display */
+.timepicker-analog-display {
+ flex: 2.5 auto;
+}
+
+.timepicker-plate {
+ background-color: $timepicker-clock-plate-bg;
+ 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: $timepicker-clock-color;
+ 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: transparentize($secondary-color, .75);
+}
+.timepicker-dial {
+ transition: transform 350ms, opacity 350ms;
+}
+.timepicker-dial-out {
+ &.timepicker-hours {
+ transform: scale(1.1, 1.1);
+ }
+
+ &.timepicker-minutes {
+ transform: scale(.8, .8);
+ }
+
+ opacity: 0;
+}
+.timepicker-canvas {
+ transition: opacity 175ms;
+
+ line {
+ stroke: $secondary-color;
+ stroke-width: 4;
+ stroke-linecap: round;
+ }
+}
+.timepicker-canvas-out {
+ opacity: 0.25;
+}
+.timepicker-canvas-bearing {
+ stroke: none;
+ fill: $secondary-color;
+}
+.timepicker-canvas-bg {
+ stroke: none;
+ fill: $secondary-color;
+}
+
+
+/* Footer */
+.timepicker-footer {
+ margin: 0 auto;
+ padding: 5px 1rem;
+ display: flex;
+ justify-content: space-between;
+}
+
+.timepicker-clear {
+ color: $error-color;
+}
+
+.timepicker-close {
+ color: $secondary-color;
+}
+
+.timepicker-clear,
+.timepicker-close {
+ padding: 0 20px;
+}
+
+/* Media Queries */
+@media #{$medium-and-up} {
+ .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;
+ }
+}
diff --git a/sass/components/_toast.scss b/sass/components/_toast.scss
new file mode 100644
index 0000000..412400f
--- /dev/null
+++ b/sass/components/_toast.scss
@@ -0,0 +1,58 @@
+#toast-container {
+ display:block;
+ position: fixed;
+ z-index: 10000;
+
+ @media #{$small-and-down} {
+ min-width: 100%;
+ bottom: 0%;
+ }
+ @media #{$medium-only} {
+ left: 5%;
+ bottom: 7%;
+ max-width: 90%;
+ }
+ @media #{$large-and-up} {
+ top: 10%;
+ right: 7%;
+ max-width: 86%;
+ }
+}
+
+.toast {
+ @extend .z-depth-1;
+ border-radius: 2px;
+ top: 35px;
+ width: auto;
+ margin-top: 10px;
+ position: relative;
+ max-width:100%;
+ height: auto;
+ min-height: $toast-height;
+ line-height: 1.5em;
+ background-color: $toast-color;
+ padding: 10px 25px;
+ font-size: 1.1rem;
+ font-weight: 300;
+ color: $toast-text-color;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: default;
+
+ .toast-action {
+ color: $toast-action-color;
+ font-weight: 500;
+ margin-right: -25px;
+ margin-left: 3rem;
+ }
+
+ &.rounded{
+ border-radius: 24px;
+ }
+
+ @media #{$small-and-down} {
+ width: 100%;
+ border-radius: 0;
+ }
+}
diff --git a/sass/components/_tooltip.scss b/sass/components/_tooltip.scss
new file mode 100644
index 0000000..5ec4299
--- /dev/null
+++ b/sass/components/_tooltip.scss
@@ -0,0 +1,32 @@
+.material-tooltip {
+ padding: 10px 8px;
+ font-size: 1rem;
+ z-index: 2000;
+ background-color: transparent;
+ border-radius: 2px;
+ color: #fff;
+ min-height: 36px;
+ line-height: 120%;
+ opacity: 0;
+ position: absolute;
+ text-align: center;
+ max-width: calc(100% - 4px);
+ overflow: hidden;
+ left: 0;
+ top: 0;
+ pointer-events: none;
+ visibility: hidden;
+ background-color: #323232;
+}
+
+.backdrop {
+ position: absolute;
+ opacity: 0;
+ height: 7px;
+ width: 14px;
+ border-radius: 0 0 50% 50%;
+ background-color: #323232;
+ z-index: -1;
+ transform-origin: 50% 0%;
+ visibility: hidden;
+}
diff --git a/sass/components/_transitions.scss b/sass/components/_transitions.scss
new file mode 100644
index 0000000..cb9f60d
--- /dev/null
+++ b/sass/components/_transitions.scss
@@ -0,0 +1,13 @@
+// Scale transition
+.scale-transition {
+ &.scale-out {
+ transform: scale(0);
+ transition: transform .2s !important;
+ }
+
+ &.scale-in {
+ transform: scale(1);
+ }
+
+ transition: transform .3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;
+} \ No newline at end of file
diff --git a/sass/components/_typography.scss b/sass/components/_typography.scss
new file mode 100644
index 0000000..b9b93b3
--- /dev/null
+++ b/sass/components/_typography.scss
@@ -0,0 +1,60 @@
+
+a {
+ text-decoration: none;
+}
+
+html{
+ line-height: 1.5;
+
+ @media only screen and (min-width: 0) {
+ font-size: 14px;
+ }
+
+ @media only screen and (min-width: $medium-screen) {
+ font-size: 14.5px;
+ }
+
+ @media only screen and (min-width: $large-screen) {
+ font-size: 15px;
+ }
+
+ font-family: $font-stack;
+ font-weight: normal;
+ color: $off-black;
+}
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+// Header Styles
+h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
+h1 { font-size: $h1-fontsize; line-height: 110%; margin: ($h1-fontsize / 1.5) 0 ($h1-fontsize / 2.5) 0;}
+h2 { font-size: $h2-fontsize; line-height: 110%; margin: ($h2-fontsize / 1.5) 0 ($h2-fontsize / 2.5) 0;}
+h3 { font-size: $h3-fontsize; line-height: 110%; margin: ($h3-fontsize / 1.5) 0 ($h3-fontsize / 2.5) 0;}
+h4 { font-size: $h4-fontsize; line-height: 110%; margin: ($h4-fontsize / 1.5) 0 ($h4-fontsize / 2.5) 0;}
+h5 { font-size: $h5-fontsize; line-height: 110%; margin: ($h5-fontsize / 1.5) 0 ($h5-fontsize / 2.5) 0;}
+h6 { font-size: $h6-fontsize; line-height: 110%; margin: ($h6-fontsize / 1.5) 0 ($h6-fontsize / 2.5) 0;}
+
+// Text Styles
+em { font-style: italic; }
+strong { font-weight: 500; }
+small { font-size: 75%; }
+.light { font-weight: 300; }
+.thin { font-weight: 200; }
+
+
+.flow-text{
+ $i: 0;
+ @while $i <= $intervals {
+ @media only screen and (min-width : 360 + ($i * $interval-size)) {
+ font-size: 1.2rem * (1 + (.02 * $i));
+ }
+ $i: $i + 1;
+ }
+
+ // Handle below 360px screen
+ @media only screen and (max-width: 360px) {
+ font-size: 1.2rem;
+ }
+}
diff --git a/sass/components/_variables.scss b/sass/components/_variables.scss
new file mode 100644
index 0000000..fa03d61
--- /dev/null
+++ b/sass/components/_variables.scss
@@ -0,0 +1,350 @@
+// ==========================================================================
+// Materialize variables
+// ==========================================================================
+//
+// Table of Contents:
+//
+// 1. Colors
+// 2. Badges
+// 3. Buttons
+// 4. Cards
+// 5. Carousel
+// 6. Collapsible
+// 7. Chips
+// 8. Date + Time Picker
+// 9. Dropdown
+// 10. Forms
+// 11. Global
+// 12. Grid
+// 13. Navigation Bar
+// 14. Side Navigation
+// 15. Photo Slider
+// 16. Spinners | Loaders
+// 17. Tabs
+// 18. Tables
+// 19. Toasts
+// 20. Typography
+// 21. Footer
+// 22. Flow Text
+// 23. Collections
+// 24. Progress Bar
+
+
+
+// 1. Colors
+// ==========================================================================
+
+$primary-color: color("materialize-red", "lighten-2") !default;
+$primary-color-light: lighten($primary-color, 15%) !default;
+$primary-color-dark: darken($primary-color, 15%) !default;
+
+$secondary-color: color("teal", "lighten-1") !default;
+$success-color: color("green", "base") !default;
+$error-color: color("red", "base") !default;
+$link-color: color("light-blue", "darken-1") !default;
+
+
+// 2. Badges
+// ==========================================================================
+
+$badge-bg-color: $secondary-color !default;
+$badge-height: 22px !default;
+
+
+// 3. Buttons
+// ==========================================================================
+
+// Shared styles
+$button-border: none !default;
+$button-background-focus: lighten($secondary-color, 4%) !default;
+$button-font-size: 14px !default;
+$button-icon-font-size: 1.3rem !default;
+$button-height: 36px !default;
+$button-padding: 0 16px !default;
+$button-radius: 2px !default;
+
+// Disabled styles
+$button-disabled-background: #DFDFDF !default;
+$button-disabled-color: #9F9F9F !default;
+
+// Raised buttons
+$button-raised-background: $secondary-color !default;
+$button-raised-background-hover: lighten($button-raised-background, 5%) !default;
+$button-raised-color: #fff !default;
+
+// Large buttons
+$button-large-font-size: 15px !default;
+$button-large-icon-font-size: 1.6rem !default;
+$button-large-height: $button-height * 1.5 !default;
+$button-floating-large-size: 56px !default;
+
+// Small buttons
+$button-small-font-size: 13px !default;
+$button-small-icon-font-size: 1.2rem !default;
+$button-small-height: $button-height * .9 !default;
+$button-floating-small-size: $button-height * .9 !default;
+
+// Flat buttons
+$button-flat-color: #343434 !default;
+$button-flat-disabled-color: lighten(#999, 10%) !default;
+
+// Floating buttons
+$button-floating-background: $secondary-color !default;
+$button-floating-background-hover: $button-floating-background !default;
+$button-floating-color: #fff !default;
+$button-floating-size: 40px !default;
+$button-floating-radius: 50% !default;
+
+
+// 4. Cards
+// ==========================================================================
+
+$card-padding: 24px !default;
+$card-bg-color: #fff !default;
+$card-link-color: color("orange", "accent-2") !default;
+$card-link-color-light: lighten($card-link-color, 20%) !default;
+
+
+// 5. Carousel
+// ==========================================================================
+
+$carousel-height: 400px !default;
+$carousel-item-height: $carousel-height / 2 !default;
+$carousel-item-width: $carousel-item-height !default;
+
+
+// 6. Collapsible
+// ==========================================================================
+
+$collapsible-height: 3rem !default;
+$collapsible-line-height: $collapsible-height !default;
+$collapsible-header-color: #fff !default;
+$collapsible-border-color: #ddd !default;
+
+
+// 7. Chips
+// ==========================================================================
+
+$chip-bg-color: #e4e4e4 !default;
+$chip-border-color: #9e9e9e !default;
+$chip-selected-color: #26a69a !default;
+$chip-margin: 5px !default;
+
+
+// 8. Date + Time Picker
+// ==========================================================================
+
+$datepicker-display-font-size: 2.8rem;
+$datepicker-calendar-header-color: #999;
+$datepicker-weekday-color: rgba(0, 0, 0, .87) !default;
+$datepicker-weekday-bg: darken($secondary-color, 7%) !default;
+$datepicker-date-bg: $secondary-color !default;
+$datepicker-year: rgba(255, 255, 255, .7) !default;
+$datepicker-focus: rgba(0,0,0, .05) !default;
+$datepicker-selected: $secondary-color !default;
+$datepicker-selected-outfocus: desaturate(lighten($secondary-color, 35%), 15%) !default;
+$datepicker-day-focus: transparentize(desaturate($secondary-color, 5%), .75) !default;
+$datepicker-disabled-day-color: rgba(0, 0, 0, .3) !default;
+
+$timepicker-clock-color: rgba(0, 0, 0, .87) !default;
+$timepicker-clock-plate-bg: #eee !default;
+
+
+// 9. Dropdown
+// ==========================================================================
+
+$dropdown-bg-color: #fff !default;
+$dropdown-hover-bg-color: #eee !default;
+$dropdown-color: $secondary-color !default;
+$dropdown-item-height: 50px !default;
+
+
+// 10. Forms
+// ==========================================================================
+
+// Text Inputs + Textarea
+$input-height: 3rem !default;
+$input-border-color: color("grey", "base") !default;
+$input-label-color: color("grey", "base") !default;
+$input-border: 1px solid $input-border-color !default;
+$input-background: #fff !default;
+$input-error-color: $error-color !default;
+$input-success-color: $success-color !default;
+$input-focus-color: $secondary-color !default;
+$input-font-size: 16px !default;
+$input-margin-bottom: 8px;
+$input-margin: 0 0 $input-margin-bottom 0 !default;
+$input-padding: 0 !default;
+$label-font-size: .8rem !default;
+$input-disabled-color: rgba(0,0,0, .42) !default;
+$input-disabled-solid-color: #949494 !default;
+$input-disabled-border: 1px dotted $input-disabled-color !default;
+$input-invalid-border: 1px solid $input-error-color !default;
+$input-icon-size: 2rem;
+$placeholder-text-color: lighten($input-border-color, 20%) !default;
+
+// Radio Buttons
+$radio-fill-color: $secondary-color !default;
+$radio-empty-color: #5a5a5a !default;
+$radio-border: 2px solid $radio-fill-color !default;
+
+// Range
+$range-height: 14px !default;
+$range-width: 14px !default;
+$track-height: 3px !default;
+
+// Select
+$select-border: 1px solid #f2f2f2 !default;
+$select-background: rgba(255, 255, 255, 0.90) !default;
+$select-focus: 1px solid lighten($secondary-color, 47%) !default;
+$select-option-hover: rgba(0,0,0,.08) !default;
+$select-option-focus: rgba(0,0,0,.08) !default;
+$select-option-selected: rgba(0,0,0,.03) !default;
+$select-padding: 5px !default;
+$select-radius: 2px !default;
+$select-disabled-color: rgba(0,0,0,.3) !default;
+
+// Switches
+$switch-bg-color: $secondary-color !default;
+$switch-checked-lever-bg: desaturate(lighten($switch-bg-color, 25%), 25%) !default;
+$switch-unchecked-bg: #F1F1F1 !default;
+$switch-unchecked-lever-bg: rgba(0,0,0,.38) !default;
+$switch-radius: 15px !default;
+
+
+// 11. Global
+// ==========================================================================
+
+// Media Query Ranges
+$small-screen-up: 601px !default;
+$medium-screen-up: 993px !default;
+$large-screen-up: 1201px !default;
+$small-screen: 600px !default;
+$medium-screen: 992px !default;
+$large-screen: 1200px !default;
+
+$medium-and-up: "only screen and (min-width : #{$small-screen-up})" !default;
+$large-and-up: "only screen and (min-width : #{$medium-screen-up})" !default;
+$extra-large-and-up: "only screen and (min-width : #{$large-screen-up})" !default;
+$small-and-down: "only screen and (max-width : #{$small-screen})" !default;
+$medium-and-down: "only screen and (max-width : #{$medium-screen})" !default;
+$medium-only: "only screen and (min-width : #{$small-screen-up}) and (max-width : #{$medium-screen})" !default;
+
+
+// 12. Grid
+// ==========================================================================
+
+$num-cols: 12 !default;
+$gutter-width: 1.5rem !default;
+$element-top-margin: $gutter-width/3 !default;
+$element-bottom-margin: ($gutter-width*2)/3 !default;
+
+
+// 13. Navigation Bar
+// ==========================================================================
+
+$navbar-height: 64px !default;
+$navbar-line-height: $navbar-height !default;
+$navbar-height-mobile: 56px !default;
+$navbar-line-height-mobile: $navbar-height-mobile !default;
+$navbar-font-size: 1rem !default;
+$navbar-font-color: #fff !default;
+$navbar-brand-font-size: 2.1rem !default;
+
+// 14. Side Navigation
+// ==========================================================================
+
+$sidenav-width: 300px !default;
+$sidenav-font-size: 14px !default;
+$sidenav-font-color: rgba(0,0,0,.87) !default;
+$sidenav-bg-color: #fff !default;
+$sidenav-padding: 16px !default;
+$sidenav-item-height: 48px !default;
+$sidenav-line-height: $sidenav-item-height !default;
+
+
+// 15. Photo Slider
+// ==========================================================================
+
+$slider-bg-color: color('grey', 'base') !default;
+$slider-bg-color-light: color('grey', 'lighten-2') !default;
+$slider-indicator-color: color('green', 'base') !default;
+
+
+// 16. Spinners | Loaders
+// ==========================================================================
+
+$spinner-default-color: $secondary-color !default;
+
+
+// 17. Tabs
+// ==========================================================================
+
+$tabs-underline-color: $primary-color-light !default;
+$tabs-text-color: $primary-color !default;
+$tabs-bg-color: #fff !default;
+
+
+// 18. Tables
+// ==========================================================================
+
+$table-border-color: rgba(0,0,0,.12) !default;
+$table-striped-color: rgba(242, 242, 242, 0.5) !default;
+
+
+// 19. Toasts
+// ==========================================================================
+
+$toast-height: 48px !default;
+$toast-color: #323232 !default;
+$toast-text-color: #fff !default;
+$toast-action-color: #eeff41;
+
+
+// 20. Typography
+// ==========================================================================
+
+$font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !default;
+$off-black: rgba(0, 0, 0, 0.87) !default;
+// Header Styles
+$h1-fontsize: 4.2rem !default;
+$h2-fontsize: 3.56rem !default;
+$h3-fontsize: 2.92rem !default;
+$h4-fontsize: 2.28rem !default;
+$h5-fontsize: 1.64rem !default;
+$h6-fontsize: 1.15rem !default;
+
+
+// 21. Footer
+// ==========================================================================
+
+$footer-font-color: #fff !default;
+$footer-bg-color: $primary-color !default;
+$footer-copyright-font-color: rgba(255,255,255,.8) !default;
+$footer-copyright-bg-color: rgba(51,51,51,.08) !default;
+
+
+// 22. Flow Text
+// ==========================================================================
+
+$range : $large-screen - $small-screen !default;
+$intervals: 20 !default;
+$interval-size: $range / $intervals !default;
+
+
+// 23. Collections
+// ==========================================================================
+
+$collection-border-color: #e0e0e0 !default;
+$collection-bg-color: #fff !default;
+$collection-active-bg-color: $secondary-color !default;
+$collection-active-color: lighten($secondary-color, 55%) !default;
+$collection-hover-bg-color: #ddd !default;
+$collection-link-color: $secondary-color !default;
+$collection-line-height: 1.5rem !default;
+
+
+// 24. Progress Bar
+// ==========================================================================
+
+$progress-bar-color: $secondary-color !default;
diff --git a/sass/components/_waves.scss b/sass/components/_waves.scss
new file mode 100644
index 0000000..b36c718
--- /dev/null
+++ b/sass/components/_waves.scss
@@ -0,0 +1,114 @@
+
+/*!
+ * Waves v0.6.0
+ * http://fian.my.id/Waves
+ *
+ * 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-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 Colors
+ &.waves-light .waves-ripple {
+ background-color: rgba(255, 255, 255, 0.45);
+ }
+ &.waves-red .waves-ripple {
+ background-color: rgba(244, 67, 54, .70);
+ }
+ &.waves-yellow .waves-ripple {
+ background-color: rgba(255, 235, 59, .70);
+ }
+ &.waves-orange .waves-ripple {
+ background-color: rgba(255, 152, 0, .70);
+ }
+ &.waves-purple .waves-ripple {
+ background-color: rgba(156, 39, 176, 0.70);
+ }
+ &.waves-green .waves-ripple {
+ background-color: rgba(76, 175, 80, 0.70);
+ }
+ &.waves-teal .waves-ripple {
+ background-color: rgba(0, 150, 136, 0.70);
+ }
+
+ // Style input button bug.
+ input[type="button"], input[type="reset"], input[type="submit"] {
+ border: 0;
+ font-style: normal;
+ font-size: inherit;
+ text-transform: inherit;
+ background: none;
+ }
+
+ 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-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;
+}
+
+/* Firefox Bug: link not triggered */
+.waves-effect .waves-ripple {
+ z-index: -1;
+} \ No newline at end of file
diff --git a/sass/components/forms/_checkboxes.scss b/sass/components/forms/_checkboxes.scss
new file mode 100644
index 0000000..ddc7d96
--- /dev/null
+++ b/sass/components/forms/_checkboxes.scss
@@ -0,0 +1,200 @@
+/* Checkboxes
+ ========================================================================== */
+
+/* Remove default checkbox */
+[type="checkbox"]:not(:checked),
+[type="checkbox"]:checked {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+// Checkbox Styles
+[type="checkbox"] {
+ // Text Label Style
+ + span:not(.lever) {
+ position: relative;
+ padding-left: 35px;
+ cursor: pointer;
+ display: inline-block;
+ height: 25px;
+ line-height: 25px;
+ font-size: 1rem;
+ user-select: none;
+ }
+
+ /* checkbox aspect */
+ + span:not(.lever):before,
+ &:not(.filled-in) + span:not(.lever):after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 18px;
+ height: 18px;
+ z-index: 0;
+ border: 2px solid $radio-empty-color;
+ border-radius: 1px;
+ margin-top: 3px;
+ transition: .2s;
+ }
+
+ &:not(.filled-in) + span:not(.lever):after {
+ border: 0;
+ transform: scale(0);
+ }
+
+ &:not(:checked):disabled + span:not(.lever):before {
+ border: none;
+ background-color: $input-disabled-color;
+ }
+
+ // Focused styles
+ &.tabbed:focus + span:not(.lever):after {
+ transform: scale(1);
+ border: 0;
+ border-radius: 50%;
+ box-shadow: 0 0 0 10px rgba(0,0,0,.1);
+ background-color: rgba(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: $radio-border;
+ border-bottom: $radio-border;
+ transform: rotate(40deg);
+ backface-visibility: hidden;
+ transform-origin: 100% 100%;
+ }
+
+ &:disabled + span:before {
+ border-right: 2px solid $input-disabled-color;
+ border-bottom: 2px solid $input-disabled-color;
+ }
+}
+
+/* Indeterminate checkbox */
+[type="checkbox"]:indeterminate {
+ + span:not(.lever):before {
+ top: -11px;
+ left: -12px;
+ width: 10px;
+ height: 22px;
+ border-top: none;
+ border-left: none;
+ border-right: $radio-border;
+ border-bottom: none;
+ transform: rotate(90deg);
+ backface-visibility: hidden;
+ transform-origin: 100% 100%;
+ }
+
+ // Disabled indeterminate
+ &:disabled + span:not(.lever):before {
+ border-right: 2px solid $input-disabled-color;
+ background-color: transparent;
+ }
+}
+
+// Filled in Style
+[type="checkbox"].filled-in {
+ // General
+ + span:not(.lever):after {
+ border-radius: 2px;
+ }
+
+ + span:not(.lever):before,
+ + span:not(.lever):after {
+ content: '';
+ left: 0;
+ position: absolute;
+ /* .1s delay is for check animation */
+ transition: border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;
+ z-index: 1;
+ }
+
+ // Unchecked style
+ &: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%;
+ }
+
+ &:not(:checked) + span:not(.lever):after {
+ height: 20px;
+ width: 20px;
+ background-color: transparent;
+ border: 2px solid $radio-empty-color;
+ top: 0px;
+ z-index: 0;
+ }
+
+ // Checked style
+ &: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 $input-background;
+ border-bottom: 2px solid $input-background;
+ transform: rotateZ(37deg);
+ transform-origin: 100% 100%;
+ }
+
+ + span:not(.lever):after {
+ top: 0;
+ width: 20px;
+ height: 20px;
+ border: 2px solid $secondary-color;
+ background-color: $secondary-color;
+ z-index: 0;
+ }
+ }
+
+ // Focused styles
+ &.tabbed:focus + span:not(.lever):after {
+ border-radius: 2px;
+ border-color: $radio-empty-color;
+ background-color: rgba(0,0,0,.1);
+ }
+
+ &.tabbed:checked:focus + span:not(.lever):after {
+ border-radius: 2px;
+ background-color: $secondary-color;
+ border-color: $secondary-color;
+ }
+
+ // Disabled style
+ &:disabled:not(:checked) + span:not(.lever):before {
+ background-color: transparent;
+ border: 2px solid transparent;
+ }
+
+ &:disabled:not(:checked) + span:not(.lever):after {
+ border-color: transparent;
+ background-color: $input-disabled-solid-color;
+ }
+
+ &:disabled:checked + span:not(.lever):before {
+ background-color: transparent;
+ }
+
+ &:disabled:checked + span:not(.lever):after {
+ background-color: $input-disabled-solid-color;
+ border-color: $input-disabled-solid-color;
+ }
+}
diff --git a/sass/components/forms/_file-input.scss b/sass/components/forms/_file-input.scss
new file mode 100644
index 0000000..e0f7ef7
--- /dev/null
+++ b/sass/components/forms/_file-input.scss
@@ -0,0 +1,44 @@
+/* File Input
+ ========================================================================== */
+
+.file-field {
+ position: relative;
+
+ .file-path-wrapper {
+ overflow: hidden;
+ padding-left: 10px;
+ }
+
+ input.file-path { width: 100%; }
+
+ .btn {
+ float: left;
+ height: $input-height;
+ line-height: $input-height;
+ }
+
+ span {
+ cursor: pointer;
+ }
+
+ input[type=file] {
+
+ // Needed to override webkit button
+ &::-webkit-file-upload-button {
+ display: none;
+ }
+
+ 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);
+ }
+}
diff --git a/sass/components/forms/_forms.scss b/sass/components/forms/_forms.scss
new file mode 100644
index 0000000..a387260
--- /dev/null
+++ b/sass/components/forms/_forms.scss
@@ -0,0 +1,22 @@
+// Remove Focus Boxes
+select:focus {
+ outline: $select-focus;
+}
+
+button:focus {
+ outline: none;
+ background-color: $button-background-focus;
+}
+
+label {
+ font-size: $label-font-size;
+ color: $input-label-color;
+}
+
+@import 'input-fields';
+@import 'radio-buttons';
+@import 'checkboxes';
+@import 'switches';
+@import 'select';
+@import 'file-input';
+@import 'range';
diff --git a/sass/components/forms/_input-fields.scss b/sass/components/forms/_input-fields.scss
new file mode 100644
index 0000000..09fa47c
--- /dev/null
+++ b/sass/components/forms/_input-fields.scss
@@ -0,0 +1,354 @@
+/* Text Inputs + Textarea
+ ========================================================================== */
+
+/* Style Placeholders */
+
+::placeholder {
+ color: $placeholder-text-color;
+}
+
+/* Text inputs */
+
+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 {
+
+ // General Styles
+ background-color: transparent;
+ border: none;
+ border-bottom: $input-border;
+ border-radius: 0;
+ outline: none;
+ height: $input-height;
+ width: 100%;
+ font-size: $input-font-size;
+ margin: $input-margin;
+ padding: $input-padding;
+ box-shadow: none;
+ box-sizing: content-box;
+ transition: box-shadow .3s, border .3s;
+
+ // Disabled input style
+ &:disabled,
+ &[readonly="readonly"] {
+ color: $input-disabled-color;
+ border-bottom: $input-disabled-border;
+ }
+
+ // Disabled label style
+ &:disabled+label,
+ &[readonly="readonly"]+label {
+ color: $input-disabled-color;
+ }
+
+ // Focused input style
+ &:focus:not([readonly]) {
+ border-bottom: 1px solid $input-focus-color;
+ box-shadow: 0 1px 0 0 $input-focus-color;
+ }
+
+ // Focused label style
+ &:focus:not([readonly])+label {
+ color: $input-focus-color;
+ }
+
+ // Hide helper text on data message
+ &.valid ~ .helper-text[data-success],
+ &:focus.valid ~ .helper-text[data-success],
+ &.invalid ~ .helper-text[data-error],
+ &:focus.invalid ~ .helper-text[data-error] {
+ @extend %hidden-text;
+ }
+
+ // Valid Input Style
+ &.valid,
+ &:focus.valid {
+ @extend %valid-input-style;
+ }
+
+ // Custom Success Message
+ &.valid ~ .helper-text:after,
+ &:focus.valid ~ .helper-text:after {
+ @extend %custom-success-message;
+ }
+ &:focus.valid ~ label {
+ color: $input-success-color;
+ }
+
+ // Invalid Input Style
+ &.invalid,
+ &:focus.invalid {
+ @extend %invalid-input-style;
+ }
+
+ // Custom Error message
+ &.invalid ~ .helper-text:after,
+ &:focus.invalid ~ .helper-text:after {
+ @extend %custom-error-message;
+ }
+ &:focus.invalid ~ label {
+ color: $input-error-color;
+ }
+
+ // Full width label when using validate for error messages
+ &.validate + label {
+ width: 100%;
+ }
+
+ // Form Message Shared Styles
+ & + label:after {
+ @extend %input-after-style;
+ }
+}
+
+
+/* Validation Sass Placeholders */
+%valid-input-style {
+ border-bottom: 1px solid $input-success-color;
+ box-shadow: 0 1px 0 0 $input-success-color;
+}
+%invalid-input-style {
+ border-bottom: $input-invalid-border;
+ box-shadow: 0 1px 0 0 $input-error-color;
+}
+%hidden-text {
+ color: transparent;
+ user-select: none;
+ pointer-events: none;
+}
+%custom-success-message {
+ content: attr(data-success);
+ color: $input-success-color;
+}
+%custom-error-message {
+ content: attr(data-error);
+ color: $input-error-color;
+}
+%input-after-style {
+ display: block;
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 0;
+ opacity: 0;
+ transition: .2s opacity ease-out, .2s color ease-out;
+}
+
+
+// Styling for input field wrapper
+.input-field {
+ // Inline styles
+ &.inline {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 5px;
+
+ input,
+ .select-dropdown {
+ margin-bottom: 1rem;
+ }
+ }
+
+ // Gutter spacing
+ &.col {
+ label {
+ left: $gutter-width / 2;
+ }
+
+ .prefix ~ label,
+ .prefix ~ .validate ~ label {
+ width: calc(100% - 3rem - #{$gutter-width});
+ }
+ }
+
+ position: relative;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+
+ & > label {
+ color: $input-label-color;
+ 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);
+
+ &:not(.label-icon).active {
+ transform: translateY(-14px) scale(.8);
+ transform-origin: 0 0;
+ }
+ }
+
+ // Autofill + date + time inputs
+ & > input[type]:-webkit-autofill:not(.browser-default):not([type="search"]) + label,
+ & > input[type=date]:not(.browser-default) + label,
+ & > input[type=time]:not(.browser-default) + label {
+ transform: translateY(-14px) scale(.8);
+ transform-origin: 0 0;
+ }
+
+ .helper-text {
+ &::after {
+ opacity: 1;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ position: relative;
+ min-height: 18px;
+ display: block;
+ font-size: 12px;
+ color: rgba(0,0,0,.54);
+ }
+
+ // Prefix Icons
+ .prefix {
+ position: absolute;
+ width: $input-height;
+ font-size: $input-icon-size;
+ transition: color .2s;
+ top: ($input-height - $input-icon-size) / 2;
+
+ &.active { color: $input-focus-color; }
+ }
+
+ .prefix ~ input,
+ .prefix ~ textarea,
+ .prefix ~ label,
+ .prefix ~ .validate ~ label,
+ .prefix ~ .helper-text,
+ .prefix ~ .autocomplete-content {
+ margin-left: 3rem;
+ width: 92%;
+ width: calc(100% - 3rem);
+ }
+
+ .prefix ~ label { margin-left: 3rem; }
+
+ @media #{$medium-and-down} {
+ .prefix ~ input {
+ width: 86%;
+ width: calc(100% - 3rem);
+ }
+ }
+
+ @media #{$small-and-down} {
+ .prefix ~ input {
+ width: 80%;
+ width: calc(100% - 3rem);
+ }
+ }
+}
+
+
+/* Search Field */
+
+.input-field input[type=search] {
+ display: block;
+ line-height: inherit;
+ transition: .3s background-color;
+
+ .nav-wrapper & {
+ height: inherit;
+ padding-left: 4rem;
+ width: calc(100% - 4rem);
+ border: 0;
+ box-shadow: none;
+ }
+
+ &:focus:not(.browser-default) {
+ background-color: $input-background;
+ border: 0;
+ box-shadow: none;
+ color: #444;
+
+ & + label i,
+ & ~ .mdi-navigation-close,
+ & ~ .material-icons {
+ color: #444;
+ }
+ }
+
+ & + .label-icon {
+ transform: none;
+ left: 1rem;
+ }
+
+ & ~ .mdi-navigation-close,
+ & ~ .material-icons {
+ position: absolute;
+ top: 0;
+ right: 1rem;
+ color: transparent;
+ cursor: pointer;
+ font-size: $input-icon-size;
+ transition: .3s color;
+ }
+}
+
+
+/* Textarea */
+
+// Default textarea
+textarea {
+ width: 100%;
+ height: $input-height;
+ background-color: transparent;
+
+ &.materialize-textarea {
+ line-height: normal;
+ overflow-y: hidden; /* prevents scroll bar flash */
+ padding: .8rem 0 .8rem 0; /* prevents text jump on Enter keypress */
+ resize: none;
+ min-height: $input-height;
+ box-sizing: border-box;
+ }
+}
+
+// For textarea autoresize
+.hiddendiv {
+ visibility: hidden;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-wrap: break-word; /* future version of deprecated 'word-wrap' */
+ padding-top: 1.2rem; /* prevents text jump on Enter keypress */
+
+ // Reduces repaints
+ position: absolute;
+ top: 0;
+ z-index: -1;
+}
+
+
+/* Autocomplete */
+.autocomplete-content {
+ li {
+ .highlight { color: #444; }
+
+ img {
+ height: $dropdown-item-height - 10;
+ width: $dropdown-item-height - 10;
+ margin: 5px 15px;
+ }
+ }
+}
+
+/* Character Counter */
+.character-counter {
+ min-height: 18px;
+}
diff --git a/sass/components/forms/_radio-buttons.scss b/sass/components/forms/_radio-buttons.scss
new file mode 100644
index 0000000..c9f7296
--- /dev/null
+++ b/sass/components/forms/_radio-buttons.scss
@@ -0,0 +1,115 @@
+/* Radio Buttons
+ ========================================================================== */
+
+// Remove default Radio Buttons
+[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;
+}
+
+/* Unchecked styles */
+[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 $radio-empty-color;
+}
+
+[type="radio"]:not(:checked) + span:after {
+ transform: scale(0);
+}
+
+/* Checked styles */
+[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: $radio-border;
+}
+
+[type="radio"]:checked + span:after,
+[type="radio"].with-gap:checked + span:after {
+ background-color: $radio-fill-color;
+}
+
+[type="radio"]:checked + span:after {
+ transform: scale(1.02);
+}
+
+/* Radio With gap */
+[type="radio"].with-gap:checked + span:after {
+ transform: scale(.5);
+}
+
+/* Focused styles */
+[type="radio"].tabbed:focus + span:before {
+ box-shadow: 0 0 0 10px rgba(0,0,0,.1);
+}
+
+/* Disabled Radio With gap */
+[type="radio"].with-gap:disabled:checked + span:before {
+ border: 2px solid $input-disabled-color;
+}
+
+[type="radio"].with-gap:disabled:checked + span:after {
+ border: none;
+ background-color: $input-disabled-color;
+}
+
+/* Disabled style */
+[type="radio"]:disabled:not(:checked) + span:before,
+[type="radio"]:disabled:checked + span:before {
+ background-color: transparent;
+ border-color: $input-disabled-color;
+}
+
+[type="radio"]:disabled + span {
+ color: $input-disabled-color;
+}
+
+[type="radio"]:disabled:not(:checked) + span:before {
+ border-color: $input-disabled-color;
+}
+
+[type="radio"]:disabled:checked + span:after {
+ background-color: $input-disabled-color;
+ border-color: $input-disabled-solid-color;
+}
diff --git a/sass/components/forms/_range.scss b/sass/components/forms/_range.scss
new file mode 100644
index 0000000..18607f5
--- /dev/null
+++ b/sass/components/forms/_range.scss
@@ -0,0 +1,161 @@
+/* Range
+ ========================================================================== */
+
+.range-field {
+ position: relative;
+}
+
+input[type=range],
+input[type=range] + .thumb {
+ @extend .no-select;
+ cursor: pointer;
+}
+
+input[type=range] {
+ position: relative;
+ background-color: transparent;
+ border: none;
+ outline: none;
+ width: 100%;
+ margin: 15px 0;
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+input[type=range] + .thumb {
+ position: absolute;
+ top: 10px;
+ left: 0;
+ border: none;
+ height: 0;
+ width: 0;
+ border-radius: 50%;
+ background-color: $radio-fill-color;
+ margin-left: 7px;
+
+ transform-origin: 50% 50%;
+ transform: rotate(-45deg);
+
+ .value {
+ display: block;
+ width: 30px;
+ text-align: center;
+ color: $radio-fill-color;
+ font-size: 0;
+ transform: rotate(45deg);
+ }
+
+ &.active {
+ border-radius: 50% 50% 50% 0;
+
+ .value {
+ color: $input-background;
+ margin-left: -1px;
+ margin-top: 8px;
+ font-size: 10px;
+ }
+ }
+}
+
+// Shared
+@mixin range-track {
+ height: $track-height;
+ background: #c2c0c2;
+ border: none;
+}
+
+@mixin range-thumb {
+ border: none;
+ height: $range-height;
+ width: $range-width;
+ border-radius: 50%;
+ background: $radio-fill-color;
+ transition: box-shadow .3s;
+}
+
+// WebKit
+input[type=range] {
+ -webkit-appearance: none;
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+ @include range-track;
+}
+
+input[type=range]::-webkit-slider-thumb {
+ @include range-thumb;
+ -webkit-appearance: none;
+ background-color: $radio-fill-color;
+ 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($radio-fill-color, .26);
+}
+
+// FireFox
+input[type=range] {
+ /* fix for FF unable to apply focus style bug */
+ border: 1px solid white;
+
+ /*required for proper track sizing in FF*/
+}
+
+input[type=range]::-moz-range-track {
+ @include range-track;
+}
+
+input[type=range]::-moz-focus-inner {
+ border: 0;
+}
+
+input[type=range]::-moz-range-thumb {
+ @include range-thumb;
+ margin-top: -5px;
+}
+
+// hide the outline behind the border
+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($radio-fill-color, .26);
+}
+
+// IE 10+
+input[type=range]::-ms-track {
+ height: $track-height;
+
+ // remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead
+ background: transparent;
+
+ // leave room for the larger thumb to overflow with a transparent border */
+ border-color: transparent;
+ border-width: 6px 0;
+
+ /*remove default tick marks*/
+ color: transparent;
+}
+
+input[type=range]::-ms-fill-lower {
+ background: #777;
+}
+
+input[type=range]::-ms-fill-upper {
+ background: #ddd;
+}
+
+input[type=range]::-ms-thumb {
+ @include range-thumb;
+}
+
+.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb {
+ box-shadow: 0 0 0 10px rgba($radio-fill-color, .26);
+}
diff --git a/sass/components/forms/_select.scss b/sass/components/forms/_select.scss
new file mode 100644
index 0000000..2fd04d3
--- /dev/null
+++ b/sass/components/forms/_select.scss
@@ -0,0 +1,180 @@
+/* Select Field
+ ========================================================================== */
+
+select { display: none; }
+select.browser-default { display: block; }
+
+select {
+ background-color: $select-background;
+ width: 100%;
+ padding: $select-padding;
+ border: $select-border;
+ border-radius: $select-radius;
+ height: $input-height;
+}
+
+.select-label {
+ position: absolute;
+}
+
+.select-wrapper {
+ &.valid .helper-text[data-success],
+ &.invalid ~ .helper-text[data-error] {
+ @extend %hidden-text;
+ }
+
+ &.valid {
+ & > input.select-dropdown {
+ @extend %valid-input-style;
+ }
+
+ & ~ .helper-text:after {
+ @extend %custom-success-message;
+ }
+ }
+
+ &.invalid {
+ & > input.select-dropdown,
+ & > input.select-dropdown:focus {
+ @extend %invalid-input-style;
+ }
+
+ & ~ .helper-text:after {
+ @extend %custom-error-message;
+ }
+ }
+
+ &.valid + label,
+ &.invalid + label {
+ width: 100%;
+ pointer-events: none;
+ }
+
+ & + label:after {
+ @extend %input-after-style;
+ }
+
+ position: relative;
+
+ input.select-dropdown {
+ &:focus {
+ border-bottom: 1px solid $input-focus-color;
+ }
+ position: relative;
+ cursor: pointer;
+ background-color: transparent;
+ border: none;
+ border-bottom: $input-border;
+ outline: none;
+ height: $input-height;
+ line-height: $input-height;
+ width: 100%;
+ font-size: $input-font-size;
+ margin: $input-margin;
+ padding: 0;
+ display: block;
+ user-select:none;
+ z-index: 1;
+ }
+
+ .caret {
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ z-index: 0;
+ fill: rgba(0,0,0,.87);
+ }
+
+ & + label {
+ position: absolute;
+ top: -26px;
+ font-size: $label-font-size;
+ }
+}
+
+// Disabled styles
+select:disabled {
+ color: $input-disabled-color;
+}
+
+.select-wrapper.disabled {
+ + label {
+ color: $input-disabled-color;
+ }
+ .caret {
+ fill: $input-disabled-color;
+ }
+}
+
+.select-wrapper input.select-dropdown:disabled {
+ color: $input-disabled-color;
+ cursor: default;
+ user-select: none;
+}
+
+.select-wrapper i {
+ color: $select-disabled-color;
+}
+
+.select-dropdown li.disabled,
+.select-dropdown li.disabled > span,
+.select-dropdown li.optgroup {
+ color: $select-disabled-color;
+ background-color: transparent;
+}
+
+body.keyboard-focused {
+ .select-dropdown.dropdown-content li:focus {
+ background-color: $select-option-focus;
+ }
+}
+
+.select-dropdown.dropdown-content {
+ li {
+ &:hover {
+ background-color: $select-option-hover;
+ }
+
+ &.selected {
+ background-color: $select-option-selected;
+ }
+ }
+}
+
+// Prefix Icons
+.prefix ~ .select-wrapper {
+ margin-left: 3rem;
+ width: 92%;
+ width: calc(100% - 3rem);
+}
+
+.prefix ~ label { margin-left: 3rem; }
+
+// Icons
+.select-dropdown li {
+ img {
+ height: $dropdown-item-height - 10;
+ width: $dropdown-item-height - 10;
+ margin: 5px 15px;
+ float: right;
+ }
+}
+
+// Optgroup styles
+.select-dropdown li.optgroup {
+ border-top: 1px solid $dropdown-hover-bg-color;
+
+ &.selected > span {
+ color: rgba(0, 0, 0, .7);
+ }
+
+ & > span {
+ color: rgba(0, 0, 0, .4);
+ }
+
+ & ~ li.optgroup-option {
+ padding-left: 1rem;
+ }
+}
diff --git a/sass/components/forms/_switches.scss b/sass/components/forms/_switches.scss
new file mode 100644
index 0000000..3296b12
--- /dev/null
+++ b/sass/components/forms/_switches.scss
@@ -0,0 +1,89 @@
+/* Switch
+ ========================================================================== */
+
+.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;
+
+ &:checked + .lever {
+ background-color: $switch-checked-lever-bg;
+
+ &:before, &:after {
+ left: 18px;
+ }
+
+ &:after {
+ background-color: $switch-bg-color;
+ }
+ }
+}
+
+.switch label .lever {
+ content: "";
+ display: inline-block;
+ position: relative;
+ width: 36px;
+ height: 14px;
+ background-color: $switch-unchecked-lever-bg;
+ border-radius: $switch-radius;
+ margin-right: 10px;
+ transition: background 0.3s ease;
+ vertical-align: middle;
+ margin: 0 16px;
+
+ &:before, &: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;
+ }
+
+ &:before {
+ background-color: transparentize($switch-bg-color, .85);
+ }
+
+ &:after {
+ background-color: $switch-unchecked-bg;
+ 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);
+ }
+}
+
+// Switch active style
+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: transparentize($switch-bg-color, .85);
+}
+
+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,.08);
+}
+
+// Disabled Styles
+.switch input[type=checkbox][disabled] + .lever {
+ cursor: default;
+ background-color: rgba(0,0,0,.12);
+}
+
+.switch label input[type=checkbox][disabled] + .lever:after,
+.switch label input[type=checkbox][disabled]:checked + .lever:after {
+ background-color: $input-disabled-solid-color;
+}
diff --git a/sass/materialize.scss b/sass/materialize.scss
new file mode 100644
index 0000000..6a51657
--- /dev/null
+++ b/sass/materialize.scss
@@ -0,0 +1,41 @@
+@charset "UTF-8";
+
+// Color
+@import "components/color-variables";
+@import "components/color-classes";
+
+// Variables;
+@import "components/variables";
+
+// Reset
+@import "components/normalize";
+
+// components
+@import "components/global";
+@import "components/badges";
+@import "components/icons-material-design";
+@import "components/grid";
+@import "components/navbar";
+@import "components/typography";
+@import "components/transitions";
+@import "components/cards";
+@import "components/toast";
+@import "components/tabs";
+@import "components/tooltip";
+@import "components/buttons";
+@import "components/dropdown";
+@import "components/waves";
+@import "components/modal";
+@import "components/collapsible";
+@import "components/chips";
+@import "components/materialbox";
+@import "components/forms/forms";
+@import "components/table_of_contents";
+@import "components/sidenav";
+@import "components/preloader";
+@import "components/slider";
+@import "components/carousel";
+@import "components/tapTarget";
+@import "components/pulse";
+@import "components/datepicker";
+@import "components/timepicker";
diff --git a/sass/package-lock.json b/sass/package-lock.json
deleted file mode 100644
index 9d745ad..0000000
--- a/sass/package-lock.json
+++ /dev/null
@@ -1,7153 +0,0 @@
-{
- "name": "travelynx",
- "version": "1.0.0",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "@csstools/convert-colors": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
- "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
- "dev": true
- },
- "@types/q": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
- "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
- "dev": true
- },
- "@webassemblyjs/ast": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
- "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==",
- "dev": true,
- "requires": {
- "@webassemblyjs/helper-module-context": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/wast-parser": "1.8.5"
- }
- },
- "@webassemblyjs/floating-point-hex-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz",
- "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==",
- "dev": true
- },
- "@webassemblyjs/helper-api-error": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz",
- "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==",
- "dev": true
- },
- "@webassemblyjs/helper-buffer": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz",
- "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==",
- "dev": true
- },
- "@webassemblyjs/helper-code-frame": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz",
- "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==",
- "dev": true,
- "requires": {
- "@webassemblyjs/wast-printer": "1.8.5"
- }
- },
- "@webassemblyjs/helper-fsm": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz",
- "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==",
- "dev": true
- },
- "@webassemblyjs/helper-module-context": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz",
- "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "mamacro": "0.0.3"
- }
- },
- "@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz",
- "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==",
- "dev": true
- },
- "@webassemblyjs/helper-wasm-section": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz",
- "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5"
- }
- },
- "@webassemblyjs/ieee754": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz",
- "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==",
- "dev": true,
- "requires": {
- "@xtuc/ieee754": "1.2.0"
- }
- },
- "@webassemblyjs/leb128": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz",
- "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==",
- "dev": true,
- "requires": {
- "@xtuc/long": "4.2.2"
- }
- },
- "@webassemblyjs/utf8": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz",
- "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==",
- "dev": true
- },
- "@webassemblyjs/wasm-edit": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz",
- "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/helper-wasm-section": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5",
- "@webassemblyjs/wasm-opt": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5",
- "@webassemblyjs/wast-printer": "1.8.5"
- }
- },
- "@webassemblyjs/wasm-gen": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz",
- "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/ieee754": "1.8.5",
- "@webassemblyjs/leb128": "1.8.5",
- "@webassemblyjs/utf8": "1.8.5"
- }
- },
- "@webassemblyjs/wasm-opt": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz",
- "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-buffer": "1.8.5",
- "@webassemblyjs/wasm-gen": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5"
- }
- },
- "@webassemblyjs/wasm-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz",
- "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-api-error": "1.8.5",
- "@webassemblyjs/helper-wasm-bytecode": "1.8.5",
- "@webassemblyjs/ieee754": "1.8.5",
- "@webassemblyjs/leb128": "1.8.5",
- "@webassemblyjs/utf8": "1.8.5"
- }
- },
- "@webassemblyjs/wast-parser": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz",
- "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/floating-point-hex-parser": "1.8.5",
- "@webassemblyjs/helper-api-error": "1.8.5",
- "@webassemblyjs/helper-code-frame": "1.8.5",
- "@webassemblyjs/helper-fsm": "1.8.5",
- "@xtuc/long": "4.2.2"
- }
- },
- "@webassemblyjs/wast-printer": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz",
- "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/wast-parser": "1.8.5",
- "@xtuc/long": "4.2.2"
- }
- },
- "@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
- "dev": true
- },
- "@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
- "dev": true
- },
- "abbrev": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "dev": true
- },
- "acorn": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
- "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
- "dev": true
- },
- "acorn-dynamic-import": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz",
- "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==",
- "dev": true
- },
- "ajv": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
- "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "2.0.1",
- "fast-json-stable-stringify": "2.0.0",
- "json-schema-traverse": "0.4.1",
- "uri-js": "4.2.2"
- }
- },
- "ajv-errors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
- "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
- "dev": true
- },
- "ajv-keywords": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz",
- "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==",
- "dev": true
- },
- "alphanum-sort": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
- "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
- "dev": true
- },
- "amdefine": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
- "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
- "dev": true
- },
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "requires": {
- "color-convert": "1.9.3"
- }
- },
- "anymatch": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
- "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
- "dev": true,
- "requires": {
- "micromatch": "3.1.10",
- "normalize-path": "2.1.1"
- },
- "dependencies": {
- "normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
- "dev": true,
- "requires": {
- "remove-trailing-separator": "1.1.0"
- }
- }
- }
- },
- "aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "dev": true
- },
- "are-we-there-yet": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "dev": true,
- "requires": {
- "delegates": "1.0.0",
- "readable-stream": "2.3.6"
- }
- },
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "requires": {
- "sprintf-js": "1.0.3"
- }
- },
- "arr-diff": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
- "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
- "dev": true
- },
- "arr-flatten": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
- "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
- "dev": true
- },
- "arr-union": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
- "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
- "dev": true
- },
- "array-find-index": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
- "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
- "dev": true
- },
- "array-unique": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
- "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
- "dev": true
- },
- "asn1": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
- "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
- "dev": true,
- "requires": {
- "safer-buffer": "2.1.2"
- }
- },
- "asn1.js": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
- "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "inherits": "2.0.3",
- "minimalistic-assert": "1.0.1"
- }
- },
- "assert": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
- "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
- "dev": true,
- "requires": {
- "object-assign": "4.1.1",
- "util": "0.10.3"
- },
- "dependencies": {
- "inherits": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
- "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
- "dev": true
- },
- "util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
- "dev": true,
- "requires": {
- "inherits": "2.0.1"
- }
- }
- }
- },
- "assert-plus": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true
- },
- "assign-symbols": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
- "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
- "dev": true
- },
- "async-each": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz",
- "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
- "dev": true
- },
- "async-foreach": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
- "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
- "dev": true
- },
- "asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
- "dev": true
- },
- "atob": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
- "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
- "dev": true
- },
- "autoprefixer": {
- "version": "9.5.1",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.5.1.tgz",
- "integrity": "sha512-KJSzkStUl3wP0D5sdMlP82Q52JLy5+atf2MHAre48+ckWkXgixmfHyWmA77wFDy6jTHU6mIgXv6hAQ2mf1PjJQ==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "caniuse-lite": "1.0.30000969",
- "normalize-range": "0.1.2",
- "num2fraction": "1.2.2",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "aws-sign2": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
- "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
- "dev": true
- },
- "aws4": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
- "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
- "dev": true
- },
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
- },
- "base": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
- "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
- "dev": true,
- "requires": {
- "cache-base": "1.0.1",
- "class-utils": "0.3.6",
- "component-emitter": "1.3.0",
- "define-property": "1.0.0",
- "isobject": "3.0.1",
- "mixin-deep": "1.3.1",
- "pascalcase": "0.1.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
- }
- }
- }
- },
- "base64-js": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
- "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==",
- "dev": true
- },
- "bcrypt-pbkdf": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
- "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
- "dev": true,
- "requires": {
- "tweetnacl": "0.14.5"
- }
- },
- "big.js": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
- "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
- "dev": true
- },
- "binary-extensions": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
- "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
- "dev": true
- },
- "block-stream": {
- "version": "0.0.9",
- "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
- "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
- "dev": true,
- "requires": {
- "inherits": "2.0.3"
- }
- },
- "bluebird": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
- "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==",
- "dev": true
- },
- "bn.js": {
- "version": "4.11.8",
- "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
- "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
- "dev": true
- },
- "boolbase": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "braces": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
- "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
- "dev": true,
- "requires": {
- "arr-flatten": "1.1.0",
- "array-unique": "0.3.2",
- "extend-shallow": "2.0.1",
- "fill-range": "4.0.0",
- "isobject": "3.0.1",
- "repeat-element": "1.1.3",
- "snapdragon": "0.8.2",
- "snapdragon-node": "2.1.1",
- "split-string": "3.1.0",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "brorand": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
- "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
- "dev": true
- },
- "browserify-aes": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
- "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
- "dev": true,
- "requires": {
- "buffer-xor": "1.0.3",
- "cipher-base": "1.0.4",
- "create-hash": "1.2.0",
- "evp_bytestokey": "1.0.3",
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "browserify-cipher": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
- "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
- "dev": true,
- "requires": {
- "browserify-aes": "1.2.0",
- "browserify-des": "1.0.2",
- "evp_bytestokey": "1.0.3"
- }
- },
- "browserify-des": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
- "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
- "dev": true,
- "requires": {
- "cipher-base": "1.0.4",
- "des.js": "1.0.0",
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "browserify-rsa": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
- "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "randombytes": "2.1.0"
- }
- },
- "browserify-sign": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
- "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "browserify-rsa": "4.0.1",
- "create-hash": "1.2.0",
- "create-hmac": "1.1.7",
- "elliptic": "6.4.1",
- "inherits": "2.0.3",
- "parse-asn1": "5.1.4"
- }
- },
- "browserify-zlib": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
- "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
- "dev": true,
- "requires": {
- "pako": "1.0.10"
- }
- },
- "browserslist": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.0.tgz",
- "integrity": "sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg==",
- "dev": true,
- "requires": {
- "caniuse-lite": "1.0.30000969",
- "electron-to-chromium": "1.3.134",
- "node-releases": "1.1.19"
- }
- },
- "buffer": {
- "version": "4.9.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
- "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
- "dev": true,
- "requires": {
- "base64-js": "1.3.0",
- "ieee754": "1.1.13",
- "isarray": "1.0.0"
- }
- },
- "buffer-from": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
- "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
- "dev": true
- },
- "buffer-xor": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
- "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
- "dev": true
- },
- "builtin-status-codes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
- "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
- "dev": true
- },
- "cacache": {
- "version": "11.3.2",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz",
- "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==",
- "dev": true,
- "requires": {
- "bluebird": "3.5.4",
- "chownr": "1.1.1",
- "figgy-pudding": "3.5.1",
- "glob": "7.1.4",
- "graceful-fs": "4.1.15",
- "lru-cache": "5.1.1",
- "mississippi": "3.0.0",
- "mkdirp": "0.5.1",
- "move-concurrently": "1.0.1",
- "promise-inflight": "1.0.1",
- "rimraf": "2.6.3",
- "ssri": "6.0.1",
- "unique-filename": "1.1.1",
- "y18n": "4.0.0"
- }
- },
- "cache-base": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
- "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
- "dev": true,
- "requires": {
- "collection-visit": "1.0.0",
- "component-emitter": "1.3.0",
- "get-value": "2.0.6",
- "has-value": "1.0.0",
- "isobject": "3.0.1",
- "set-value": "2.0.0",
- "to-object-path": "0.3.0",
- "union-value": "1.0.0",
- "unset-value": "1.0.0"
- }
- },
- "caller-callsite": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
- "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
- "dev": true,
- "requires": {
- "callsites": "2.0.0"
- }
- },
- "caller-path": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
- "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
- "dev": true,
- "requires": {
- "caller-callsite": "2.0.0"
- }
- },
- "callsites": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
- "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
- "dev": true
- },
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "dev": true
- },
- "camelcase-keys": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
- "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
- "dev": true,
- "requires": {
- "camelcase": "2.1.1",
- "map-obj": "1.0.1"
- },
- "dependencies": {
- "camelcase": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
- "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
- "dev": true
- }
- }
- },
- "caniuse-api": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
- "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "caniuse-lite": "1.0.30000969",
- "lodash.memoize": "4.1.2",
- "lodash.uniq": "4.5.0"
- }
- },
- "caniuse-lite": {
- "version": "1.0.30000969",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000969.tgz",
- "integrity": "sha512-Kus0yxkoAJgVc0bax7S4gLSlFifCa7MnSZL9p9VuS/HIKEL4seaqh28KIQAAO50cD/rJ5CiJkJFapkdDAlhFxQ==",
- "dev": true
- },
- "caseless": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
- "dev": true
- },
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "3.2.1",
- "escape-string-regexp": "1.0.5",
- "supports-color": "5.5.0"
- },
- "dependencies": {
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
- }
- }
- }
- },
- "chokidar": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
- "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==",
- "dev": true,
- "requires": {
- "anymatch": "2.0.0",
- "async-each": "1.0.3",
- "braces": "2.3.2",
- "fsevents": "1.2.9",
- "glob-parent": "3.1.0",
- "inherits": "2.0.3",
- "is-binary-path": "1.0.1",
- "is-glob": "4.0.1",
- "normalize-path": "3.0.0",
- "path-is-absolute": "1.0.1",
- "readdirp": "2.2.1",
- "upath": "1.1.2"
- }
- },
- "chownr": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
- "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
- "dev": true
- },
- "chrome-trace-event": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz",
- "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==",
- "dev": true,
- "requires": {
- "tslib": "1.9.3"
- }
- },
- "cipher-base": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
- "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "class-utils": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
- "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
- "dev": true,
- "requires": {
- "arr-union": "3.1.0",
- "define-property": "0.2.5",
- "isobject": "3.0.1",
- "static-extend": "0.1.2"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- }
- }
- },
- "cliui": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
- "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
- "dev": true,
- "requires": {
- "string-width": "2.1.1",
- "strip-ansi": "4.0.0",
- "wrap-ansi": "2.1.0"
- }
- },
- "clone-deep": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
- "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
- "dev": true,
- "requires": {
- "for-own": "1.0.0",
- "is-plain-object": "2.0.4",
- "kind-of": "6.0.2",
- "shallow-clone": "1.0.0"
- }
- },
- "coa": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
- "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
- "dev": true,
- "requires": {
- "@types/q": "1.5.2",
- "chalk": "2.4.2",
- "q": "1.5.1"
- }
- },
- "code-point-at": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
- },
- "collection-visit": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
- "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
- "dev": true,
- "requires": {
- "map-visit": "1.0.0",
- "object-visit": "1.0.1"
- }
- },
- "color": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/color/-/color-3.1.1.tgz",
- "integrity": "sha512-PvUltIXRjehRKPSy89VnDWFKY58xyhTLyxIg21vwQBI6qLwZNPmC8k3C1uytIgFKEpOIzN4y32iPm8231zFHIg==",
- "dev": true,
- "requires": {
- "color-convert": "1.9.3",
- "color-string": "1.5.3"
- }
- },
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "requires": {
- "color-name": "1.1.3"
- }
- },
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
- "dev": true
- },
- "color-string": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
- "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
- "dev": true,
- "requires": {
- "color-name": "1.1.3",
- "simple-swizzle": "0.2.2"
- }
- },
- "combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
- "requires": {
- "delayed-stream": "1.0.0"
- }
- },
- "commander": {
- "version": "2.20.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
- "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
- "dev": true
- },
- "commondir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
- "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
- "dev": true
- },
- "component-emitter": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
- "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
- "dev": true
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "dev": true,
- "requires": {
- "buffer-from": "1.1.1",
- "inherits": "2.0.3",
- "readable-stream": "2.3.6",
- "typedarray": "0.0.6"
- }
- },
- "console-browserify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
- "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
- "dev": true,
- "requires": {
- "date-now": "0.1.4"
- }
- },
- "console-control-strings": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true
- },
- "constants-browserify": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
- "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
- "dev": true
- },
- "copy-concurrently": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz",
- "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==",
- "dev": true,
- "requires": {
- "aproba": "1.2.0",
- "fs-write-stream-atomic": "1.0.10",
- "iferr": "0.1.5",
- "mkdirp": "0.5.1",
- "rimraf": "2.6.3",
- "run-queue": "1.0.3"
- }
- },
- "copy-descriptor": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
- "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
- "dev": true
- },
- "core-util-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
- "dev": true
- },
- "cosmiconfig": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz",
- "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==",
- "dev": true,
- "requires": {
- "is-directory": "0.3.1",
- "js-yaml": "3.13.1",
- "parse-json": "4.0.0",
- "require-from-string": "2.0.2"
- }
- },
- "create-ecdh": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
- "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "elliptic": "6.4.1"
- }
- },
- "create-hash": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
- "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
- "dev": true,
- "requires": {
- "cipher-base": "1.0.4",
- "inherits": "2.0.3",
- "md5.js": "1.3.5",
- "ripemd160": "2.0.2",
- "sha.js": "2.4.11"
- }
- },
- "create-hmac": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
- "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
- "dev": true,
- "requires": {
- "cipher-base": "1.0.4",
- "create-hash": "1.2.0",
- "inherits": "2.0.3",
- "ripemd160": "2.0.2",
- "safe-buffer": "5.1.2",
- "sha.js": "2.4.11"
- }
- },
- "cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
- "dev": true,
- "requires": {
- "nice-try": "1.0.5",
- "path-key": "2.0.1",
- "semver": "5.7.0",
- "shebang-command": "1.2.0",
- "which": "1.3.1"
- }
- },
- "crypto-browserify": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
- "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
- "dev": true,
- "requires": {
- "browserify-cipher": "1.0.1",
- "browserify-sign": "4.0.4",
- "create-ecdh": "4.0.3",
- "create-hash": "1.2.0",
- "create-hmac": "1.1.7",
- "diffie-hellman": "5.0.3",
- "inherits": "2.0.3",
- "pbkdf2": "3.0.17",
- "public-encrypt": "4.0.3",
- "randombytes": "2.1.0",
- "randomfill": "1.0.4"
- }
- },
- "css-blank-pseudo": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
- "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "css-color-names": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
- "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
- "dev": true
- },
- "css-declaration-sorter": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz",
- "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "timsort": "0.3.0"
- }
- },
- "css-has-pseudo": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz",
- "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0"
- }
- },
- "css-loader": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz",
- "integrity": "sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==",
- "dev": true,
- "requires": {
- "camelcase": "5.3.1",
- "icss-utils": "4.1.0",
- "loader-utils": "1.2.3",
- "normalize-path": "3.0.0",
- "postcss": "7.0.16",
- "postcss-modules-extract-imports": "2.0.0",
- "postcss-modules-local-by-default": "2.0.6",
- "postcss-modules-scope": "2.1.0",
- "postcss-modules-values": "2.0.0",
- "postcss-value-parser": "3.3.1",
- "schema-utils": "1.0.0"
- }
- },
- "css-prefers-color-scheme": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz",
- "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "css-select": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz",
- "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==",
- "dev": true,
- "requires": {
- "boolbase": "1.0.0",
- "css-what": "2.1.3",
- "domutils": "1.7.0",
- "nth-check": "1.0.2"
- }
- },
- "css-select-base-adapter": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
- "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
- "dev": true
- },
- "css-tree": {
- "version": "1.0.0-alpha.28",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.28.tgz",
- "integrity": "sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==",
- "dev": true,
- "requires": {
- "mdn-data": "1.1.4",
- "source-map": "0.5.7"
- },
- "dependencies": {
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
- }
- },
- "css-unit-converter": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz",
- "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=",
- "dev": true
- },
- "css-url-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/css-url-regex/-/css-url-regex-1.1.0.tgz",
- "integrity": "sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=",
- "dev": true
- },
- "css-what": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
- "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
- "dev": true
- },
- "cssdb": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz",
- "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==",
- "dev": true
- },
- "cssesc": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
- "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
- "dev": true
- },
- "cssnano": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
- "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
- "dev": true,
- "requires": {
- "cosmiconfig": "5.2.1",
- "cssnano-preset-default": "4.0.7",
- "is-resolvable": "1.1.0",
- "postcss": "7.0.16"
- },
- "dependencies": {
- "cosmiconfig": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz",
- "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==",
- "dev": true,
- "requires": {
- "import-fresh": "2.0.0",
- "is-directory": "0.3.1",
- "js-yaml": "3.13.1",
- "parse-json": "4.0.0"
- }
- }
- }
- },
- "cssnano-preset-default": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz",
- "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==",
- "dev": true,
- "requires": {
- "css-declaration-sorter": "4.0.1",
- "cssnano-util-raw-cache": "4.0.1",
- "postcss": "7.0.16",
- "postcss-calc": "7.0.1",
- "postcss-colormin": "4.0.3",
- "postcss-convert-values": "4.0.1",
- "postcss-discard-comments": "4.0.2",
- "postcss-discard-duplicates": "4.0.2",
- "postcss-discard-empty": "4.0.1",
- "postcss-discard-overridden": "4.0.1",
- "postcss-merge-longhand": "4.0.11",
- "postcss-merge-rules": "4.0.3",
- "postcss-minify-font-values": "4.0.2",
- "postcss-minify-gradients": "4.0.2",
- "postcss-minify-params": "4.0.2",
- "postcss-minify-selectors": "4.0.2",
- "postcss-normalize-charset": "4.0.1",
- "postcss-normalize-display-values": "4.0.2",
- "postcss-normalize-positions": "4.0.2",
- "postcss-normalize-repeat-style": "4.0.2",
- "postcss-normalize-string": "4.0.2",
- "postcss-normalize-timing-functions": "4.0.2",
- "postcss-normalize-unicode": "4.0.1",
- "postcss-normalize-url": "4.0.1",
- "postcss-normalize-whitespace": "4.0.2",
- "postcss-ordered-values": "4.1.2",
- "postcss-reduce-initial": "4.0.3",
- "postcss-reduce-transforms": "4.0.2",
- "postcss-svgo": "4.0.2",
- "postcss-unique-selectors": "4.0.1"
- }
- },
- "cssnano-util-get-arguments": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz",
- "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=",
- "dev": true
- },
- "cssnano-util-get-match": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz",
- "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=",
- "dev": true
- },
- "cssnano-util-raw-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz",
- "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "cssnano-util-same-parent": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz",
- "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==",
- "dev": true
- },
- "csso": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
- "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==",
- "dev": true,
- "requires": {
- "css-tree": "1.0.0-alpha.29"
- },
- "dependencies": {
- "css-tree": {
- "version": "1.0.0-alpha.29",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz",
- "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==",
- "dev": true,
- "requires": {
- "mdn-data": "1.1.4",
- "source-map": "0.5.7"
- }
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
- }
- },
- "currently-unhandled": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
- "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
- "dev": true,
- "requires": {
- "array-find-index": "1.0.2"
- }
- },
- "cyclist": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
- "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
- "dev": true
- },
- "dashdash": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
- "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0"
- }
- },
- "date-now": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
- "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
- "dev": true
- },
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
- },
- "decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
- "dev": true
- },
- "define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
- "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "dev": true,
- "requires": {
- "object-keys": "1.1.1"
- }
- },
- "define-property": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
- "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2",
- "isobject": "3.0.1"
- },
- "dependencies": {
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
- }
- }
- }
- },
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
- },
- "delegates": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "dev": true
- },
- "des.js": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
- "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "minimalistic-assert": "1.0.1"
- }
- },
- "detect-file": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
- "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
- "dev": true
- },
- "diffie-hellman": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
- "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "miller-rabin": "4.0.1",
- "randombytes": "2.1.0"
- }
- },
- "dom-serializer": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
- "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
- "dev": true,
- "requires": {
- "domelementtype": "1.3.1",
- "entities": "1.1.2"
- }
- },
- "domain-browser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
- "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
- "dev": true
- },
- "domelementtype": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
- "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
- "dev": true
- },
- "domutils": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
- "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
- "dev": true,
- "requires": {
- "dom-serializer": "0.1.1",
- "domelementtype": "1.3.1"
- }
- },
- "dot-prop": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
- "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
- "dev": true,
- "requires": {
- "is-obj": "1.0.1"
- }
- },
- "duplexify": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
- "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
- "dev": true,
- "requires": {
- "end-of-stream": "1.4.1",
- "inherits": "2.0.3",
- "readable-stream": "2.3.6",
- "stream-shift": "1.0.0"
- }
- },
- "ecc-jsbn": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
- "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
- "dev": true,
- "requires": {
- "jsbn": "0.1.1",
- "safer-buffer": "2.1.2"
- }
- },
- "electron-to-chromium": {
- "version": "1.3.134",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.134.tgz",
- "integrity": "sha512-C3uK2SrtWg/gSWaluLHWSHjyebVZCe4ZC0NVgTAoTq8tCR9FareRK5T7R7AS/nPZShtlEcjVMX1kQ8wi4nU68w==",
- "dev": true
- },
- "elliptic": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
- "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "brorand": "1.1.0",
- "hash.js": "1.1.7",
- "hmac-drbg": "1.0.1",
- "inherits": "2.0.3",
- "minimalistic-assert": "1.0.1",
- "minimalistic-crypto-utils": "1.0.1"
- }
- },
- "emojis-list": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
- "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
- "dev": true
- },
- "end-of-stream": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
- "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
- "dev": true,
- "requires": {
- "once": "1.4.0"
- }
- },
- "enhanced-resolve": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
- "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "memory-fs": "0.4.1",
- "tapable": "1.1.3"
- }
- },
- "entities": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
- "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
- "dev": true
- },
- "errno": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
- "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
- "dev": true,
- "requires": {
- "prr": "1.0.1"
- }
- },
- "error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
- "requires": {
- "is-arrayish": "0.2.1"
- }
- },
- "es-abstract": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
- "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
- "dev": true,
- "requires": {
- "es-to-primitive": "1.2.0",
- "function-bind": "1.1.1",
- "has": "1.0.3",
- "is-callable": "1.1.4",
- "is-regex": "1.0.4",
- "object-keys": "1.1.1"
- }
- },
- "es-to-primitive": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
- "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
- "dev": true,
- "requires": {
- "is-callable": "1.1.4",
- "is-date-object": "1.0.1",
- "is-symbol": "1.0.2"
- }
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "dev": true
- },
- "eslint-scope": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
- "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
- "dev": true,
- "requires": {
- "esrecurse": "4.2.1",
- "estraverse": "4.2.0"
- }
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true
- },
- "esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
- "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
- "dev": true,
- "requires": {
- "estraverse": "4.2.0"
- }
- },
- "estraverse": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
- "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
- "dev": true
- },
- "events": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
- "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
- "dev": true
- },
- "evp_bytestokey": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
- "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
- "dev": true,
- "requires": {
- "md5.js": "1.3.5",
- "safe-buffer": "5.1.2"
- }
- },
- "execa": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
- "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
- "dev": true,
- "requires": {
- "cross-spawn": "6.0.5",
- "get-stream": "4.1.0",
- "is-stream": "1.1.0",
- "npm-run-path": "2.0.2",
- "p-finally": "1.0.0",
- "signal-exit": "3.0.2",
- "strip-eof": "1.0.0"
- }
- },
- "expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
- "dev": true,
- "requires": {
- "debug": "2.6.9",
- "define-property": "0.2.5",
- "extend-shallow": "2.0.1",
- "posix-character-classes": "0.1.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "expand-tilde": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
- "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
- "dev": true,
- "requires": {
- "homedir-polyfill": "1.0.3"
- }
- },
- "extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true
- },
- "extend-shallow": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
- "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
- "dev": true,
- "requires": {
- "assign-symbols": "1.0.0",
- "is-extendable": "1.0.1"
- },
- "dependencies": {
- "is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
- "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
- "dev": true,
- "requires": {
- "is-plain-object": "2.0.4"
- }
- }
- }
- },
- "extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
- "dev": true,
- "requires": {
- "array-unique": "0.3.2",
- "define-property": "1.0.0",
- "expand-brackets": "2.1.4",
- "extend-shallow": "2.0.1",
- "fragment-cache": "0.2.1",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
- }
- }
- }
- },
- "extsprintf": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
- "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
- "dev": true
- },
- "fast-deep-equal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
- "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
- "dev": true
- },
- "fast-json-stable-stringify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
- "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
- "dev": true
- },
- "figgy-pudding": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
- "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
- "dev": true
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-number": "3.0.0",
- "repeat-string": "1.6.1",
- "to-regex-range": "2.1.1"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "find-cache-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz",
- "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==",
- "dev": true,
- "requires": {
- "commondir": "1.0.1",
- "make-dir": "2.1.0",
- "pkg-dir": "3.0.0"
- }
- },
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "dev": true,
- "requires": {
- "locate-path": "3.0.0"
- }
- },
- "findup-sync": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
- "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
- "dev": true,
- "requires": {
- "detect-file": "1.0.0",
- "is-glob": "3.1.0",
- "micromatch": "3.1.10",
- "resolve-dir": "1.0.1"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
- }
- },
- "flatten": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
- "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
- "dev": true
- },
- "flush-write-stream": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
- "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "readable-stream": "2.3.6"
- }
- },
- "for-in": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
- "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
- "dev": true
- },
- "for-own": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
- "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
- "dev": true,
- "requires": {
- "for-in": "1.0.2"
- }
- },
- "forever-agent": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
- "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
- "dev": true
- },
- "form-data": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
- "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
- "dev": true,
- "requires": {
- "asynckit": "0.4.0",
- "combined-stream": "1.0.8",
- "mime-types": "2.1.24"
- }
- },
- "fragment-cache": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
- "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
- "dev": true,
- "requires": {
- "map-cache": "0.2.2"
- }
- },
- "from2": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
- "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "readable-stream": "2.3.6"
- }
- },
- "fs-write-stream-atomic": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
- "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "iferr": "0.1.5",
- "imurmurhash": "0.1.4",
- "readable-stream": "2.3.6"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "fsevents": {
- "version": "1.2.9",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
- "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
- "dev": true,
- "optional": true,
- "requires": {
- "nan": "2.13.2",
- "node-pre-gyp": "0.12.0"
- },
- "dependencies": {
- "abbrev": {
- "version": "1.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "ansi-regex": {
- "version": "2.1.1",
- "bundled": true,
- "dev": true
- },
- "aproba": {
- "version": "1.2.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "are-we-there-yet": {
- "version": "1.1.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
- "balanced-match": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "bundled": true,
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "chownr": {
- "version": "1.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "code-point-at": {
- "version": "1.1.0",
- "bundled": true,
- "dev": true
- },
- "concat-map": {
- "version": "0.0.1",
- "bundled": true,
- "dev": true
- },
- "console-control-strings": {
- "version": "1.1.0",
- "bundled": true,
- "dev": true
- },
- "core-util-is": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "debug": {
- "version": "4.1.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "deep-extend": {
- "version": "0.6.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "delegates": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "detect-libc": {
- "version": "1.0.3",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "fs-minipass": {
- "version": "1.2.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "minipass": "^2.2.1"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "gauge": {
- "version": "2.7.4",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "aproba": "^1.0.3",
- "console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.0",
- "object-assign": "^4.1.0",
- "signal-exit": "^3.0.0",
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1",
- "wide-align": "^1.1.0"
- }
- },
- "glob": {
- "version": "7.1.3",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "has-unicode": {
- "version": "2.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "iconv-lite": {
- "version": "0.4.24",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "ignore-walk": {
- "version": "3.0.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "minimatch": "^3.0.4"
- }
- },
- "inflight": {
- "version": "1.0.6",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "bundled": true,
- "dev": true
- },
- "ini": {
- "version": "1.3.5",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "requires": {
- "number-is-nan": "^1.0.0"
- }
- },
- "isarray": {
- "version": "1.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "minimatch": {
- "version": "3.0.4",
- "bundled": true,
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "0.0.8",
- "bundled": true,
- "dev": true
- },
- "minipass": {
- "version": "2.3.5",
- "bundled": true,
- "dev": true,
- "requires": {
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.0"
- }
- },
- "minizlib": {
- "version": "1.2.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "minipass": "^2.2.1"
- }
- },
- "mkdirp": {
- "version": "0.5.1",
- "bundled": true,
- "dev": true,
- "requires": {
- "minimist": "0.0.8"
- }
- },
- "ms": {
- "version": "2.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "needle": {
- "version": "2.3.0",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "debug": "^4.1.0",
- "iconv-lite": "^0.4.4",
- "sax": "^1.2.4"
- }
- },
- "node-pre-gyp": {
- "version": "0.12.0",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "detect-libc": "^1.0.2",
- "mkdirp": "^0.5.1",
- "needle": "^2.2.1",
- "nopt": "^4.0.1",
- "npm-packlist": "^1.1.6",
- "npmlog": "^4.0.2",
- "rc": "^1.2.7",
- "rimraf": "^2.6.1",
- "semver": "^5.3.0",
- "tar": "^4"
- }
- },
- "nopt": {
- "version": "4.0.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "abbrev": "1",
- "osenv": "^0.1.4"
- }
- },
- "npm-bundled": {
- "version": "1.0.6",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "npm-packlist": {
- "version": "1.4.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "ignore-walk": "^3.0.1",
- "npm-bundled": "^1.0.1"
- }
- },
- "npmlog": {
- "version": "4.1.2",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
- }
- },
- "number-is-nan": {
- "version": "1.0.1",
- "bundled": true,
- "dev": true
- },
- "object-assign": {
- "version": "4.1.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "once": {
- "version": "1.4.0",
- "bundled": true,
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "os-homedir": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "osenv": {
- "version": "0.1.5",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "os-homedir": "^1.0.0",
- "os-tmpdir": "^1.0.0"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "process-nextick-args": {
- "version": "2.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "rc": {
- "version": "1.2.8",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "dependencies": {
- "minimist": {
- "version": "1.2.0",
- "bundled": true,
- "dev": true,
- "optional": true
- }
- }
- },
- "readable-stream": {
- "version": "2.3.6",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "rimraf": {
- "version": "2.6.3",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "bundled": true,
- "dev": true
- },
- "safer-buffer": {
- "version": "2.1.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "sax": {
- "version": "1.2.4",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "semver": {
- "version": "5.7.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "set-blocking": {
- "version": "2.0.0",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "signal-exit": {
- "version": "3.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "string-width": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
- }
- },
- "string_decoder": {
- "version": "1.1.1",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "bundled": true,
- "dev": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "strip-json-comments": {
- "version": "2.0.1",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "tar": {
- "version": "4.4.8",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.3.4",
- "minizlib": "^1.1.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.2"
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true,
- "optional": true
- },
- "wide-align": {
- "version": "1.1.3",
- "bundled": true,
- "dev": true,
- "optional": true,
- "requires": {
- "string-width": "^1.0.2 || 2"
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "bundled": true,
- "dev": true
- },
- "yallist": {
- "version": "3.0.3",
- "bundled": true,
- "dev": true
- }
- }
- },
- "fstream": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
- "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "inherits": "2.0.3",
- "mkdirp": "0.5.1",
- "rimraf": "2.6.3"
- }
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
- },
- "gauge": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
- "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "dev": true,
- "requires": {
- "aproba": "1.2.0",
- "console-control-strings": "1.1.0",
- "has-unicode": "2.0.1",
- "object-assign": "4.1.1",
- "signal-exit": "3.0.2",
- "string-width": "1.0.2",
- "strip-ansi": "3.0.1",
- "wide-align": "1.1.3"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
- "requires": {
- "number-is-nan": "1.0.1"
- }
- },
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
- "requires": {
- "code-point-at": "1.1.0",
- "is-fullwidth-code-point": "1.0.0",
- "strip-ansi": "3.0.1"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "2.1.1"
- }
- }
- }
- },
- "gaze": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
- "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
- "dev": true,
- "requires": {
- "globule": "1.2.1"
- }
- },
- "get-caller-file": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
- "dev": true
- },
- "get-stdin": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
- "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
- "dev": true
- },
- "get-stream": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
- "dev": true,
- "requires": {
- "pump": "3.0.0"
- }
- },
- "get-value": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
- "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
- "dev": true
- },
- "getpass": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
- "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0"
- }
- },
- "glob": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
- "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
- "dev": true,
- "requires": {
- "fs.realpath": "1.0.0",
- "inflight": "1.0.6",
- "inherits": "2.0.3",
- "minimatch": "3.0.4",
- "once": "1.4.0",
- "path-is-absolute": "1.0.1"
- }
- },
- "glob-parent": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
- "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
- "dev": true,
- "requires": {
- "is-glob": "3.1.0",
- "path-dirname": "1.0.2"
- },
- "dependencies": {
- "is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- }
- }
- },
- "global-modules": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
- "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
- "dev": true,
- "requires": {
- "global-prefix": "1.0.2",
- "is-windows": "1.0.2",
- "resolve-dir": "1.0.1"
- }
- },
- "global-prefix": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
- "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
- "dev": true,
- "requires": {
- "expand-tilde": "2.0.2",
- "homedir-polyfill": "1.0.3",
- "ini": "1.3.5",
- "is-windows": "1.0.2",
- "which": "1.3.1"
- }
- },
- "globule": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
- "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
- "dev": true,
- "requires": {
- "glob": "7.1.4",
- "lodash": "4.17.11",
- "minimatch": "3.0.4"
- }
- },
- "graceful-fs": {
- "version": "4.1.15",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
- "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
- "dev": true
- },
- "har-schema": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
- "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
- "dev": true
- },
- "har-validator": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
- "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
- "dev": true,
- "requires": {
- "ajv": "6.10.0",
- "har-schema": "2.0.0"
- }
- },
- "has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
- "requires": {
- "function-bind": "1.1.1"
- }
- },
- "has-ansi": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
- "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
- "dev": true,
- "requires": {
- "ansi-regex": "2.1.1"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- }
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "has-symbols": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
- "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
- "dev": true
- },
- "has-unicode": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
- "dev": true
- },
- "has-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
- "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
- "dev": true,
- "requires": {
- "get-value": "2.0.6",
- "has-values": "1.0.0",
- "isobject": "3.0.1"
- }
- },
- "has-values": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
- "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
- "dev": true,
- "requires": {
- "is-number": "3.0.0",
- "kind-of": "4.0.0"
- },
- "dependencies": {
- "kind-of": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
- "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "hash-base": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
- "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "hash.js": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
- "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "minimalistic-assert": "1.0.1"
- }
- },
- "hex-color-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
- "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==",
- "dev": true
- },
- "hmac-drbg": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
- "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
- "dev": true,
- "requires": {
- "hash.js": "1.1.7",
- "minimalistic-assert": "1.0.1",
- "minimalistic-crypto-utils": "1.0.1"
- }
- },
- "homedir-polyfill": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
- "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
- "dev": true,
- "requires": {
- "parse-passwd": "1.0.0"
- }
- },
- "hosted-git-info": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
- "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
- "dev": true
- },
- "hsl-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
- "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=",
- "dev": true
- },
- "hsla-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz",
- "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
- "dev": true
- },
- "html-comment-regex": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
- "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
- "dev": true
- },
- "http-signature": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
- "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0",
- "jsprim": "1.4.1",
- "sshpk": "1.16.1"
- }
- },
- "https-browserify": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
- "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
- "dev": true
- },
- "icss-replace-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
- "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
- "dev": true
- },
- "icss-utils": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.0.tgz",
- "integrity": "sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "ieee754": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
- "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
- "dev": true
- },
- "iferr": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
- "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
- "dev": true
- },
- "import-cwd": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
- "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=",
- "dev": true,
- "requires": {
- "import-from": "2.1.0"
- }
- },
- "import-fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
- "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
- "dev": true,
- "requires": {
- "caller-path": "2.0.0",
- "resolve-from": "3.0.0"
- }
- },
- "import-from": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz",
- "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=",
- "dev": true,
- "requires": {
- "resolve-from": "3.0.0"
- }
- },
- "import-local": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",
- "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==",
- "dev": true,
- "requires": {
- "pkg-dir": "3.0.0",
- "resolve-cwd": "2.0.0"
- }
- },
- "imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
- "dev": true
- },
- "in-publish": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
- "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
- "dev": true
- },
- "indent-string": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
- "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
- "dev": true,
- "requires": {
- "repeating": "2.0.1"
- }
- },
- "indexes-of": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
- "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
- "dev": true
- },
- "indexof": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
- "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
- "dev": true
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "requires": {
- "once": "1.4.0",
- "wrappy": "1.0.2"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
- },
- "ini": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
- "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
- "dev": true
- },
- "interpret": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
- "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
- "dev": true
- },
- "invert-kv": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
- "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
- "dev": true
- },
- "is-absolute-url": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz",
- "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=",
- "dev": true
- },
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
- "dev": true
- },
- "is-binary-path": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
- "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
- "dev": true,
- "requires": {
- "binary-extensions": "1.13.1"
- }
- },
- "is-buffer": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
- "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
- "dev": true
- },
- "is-callable": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
- "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
- "dev": true
- },
- "is-color-stop": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
- "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=",
- "dev": true,
- "requires": {
- "css-color-names": "0.0.4",
- "hex-color-regex": "1.1.0",
- "hsl-regex": "1.0.0",
- "hsla-regex": "1.0.0",
- "rgb-regex": "1.0.1",
- "rgba-regex": "1.0.0"
- }
- },
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-date-object": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
- "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
- "dev": true
- },
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
- "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "0.1.6",
- "is-data-descriptor": "0.1.4",
- "kind-of": "5.1.0"
- },
- "dependencies": {
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
- }
- },
- "is-directory": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
- "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
- "dev": true
- },
- "is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
- "dev": true
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-finite": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
- "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
- "dev": true,
- "requires": {
- "number-is-nan": "1.0.1"
- }
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
- "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
- "requires": {
- "is-extglob": "2.1.1"
- }
- },
- "is-number": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
- "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "is-obj": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
- "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
- "dev": true
- },
- "is-plain-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
- "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
- "dev": true
- },
- "is-plain-object": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
- "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
- "dev": true,
- "requires": {
- "isobject": "3.0.1"
- }
- },
- "is-regex": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
- "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
- "dev": true,
- "requires": {
- "has": "1.0.3"
- }
- },
- "is-resolvable": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
- "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
- "dev": true
- },
- "is-stream": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
- "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
- "dev": true
- },
- "is-svg": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
- "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==",
- "dev": true,
- "requires": {
- "html-comment-regex": "1.1.2"
- }
- },
- "is-symbol": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
- "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
- "dev": true,
- "requires": {
- "has-symbols": "1.0.0"
- }
- },
- "is-typedarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
- "dev": true
- },
- "is-utf8": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
- "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
- "dev": true
- },
- "is-windows": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
- "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
- "dev": true
- },
- "is-wsl": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
- "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
- "dev": true
- },
- "isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
- },
- "isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
- },
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
- },
- "isstream": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
- "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
- "dev": true
- },
- "js-base64": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
- "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
- "dev": true
- },
- "js-yaml": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
- "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
- "dev": true,
- "requires": {
- "argparse": "1.0.10",
- "esprima": "4.0.1"
- }
- },
- "jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
- "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
- "dev": true
- },
- "json-parse-better-errors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
- "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
- "dev": true
- },
- "json-schema": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
- "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
- "dev": true
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
- "json-stringify-safe": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
- "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
- "dev": true
- },
- "json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
- "dev": true,
- "requires": {
- "minimist": "1.2.0"
- }
- },
- "jsprim": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
- "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0",
- "extsprintf": "1.3.0",
- "json-schema": "0.2.3",
- "verror": "1.10.0"
- }
- },
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- },
- "lcid": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
- "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
- "dev": true,
- "requires": {
- "invert-kv": "2.0.0"
- }
- },
- "load-json-file": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
- "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "parse-json": "2.2.0",
- "pify": "2.3.0",
- "pinkie-promise": "2.0.1",
- "strip-bom": "2.0.0"
- },
- "dependencies": {
- "parse-json": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
- "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
- "dev": true,
- "requires": {
- "error-ex": "1.3.2"
- }
- },
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
- "loader-runner": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
- "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==",
- "dev": true
- },
- "loader-utils": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
- "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
- "dev": true,
- "requires": {
- "big.js": "5.2.2",
- "emojis-list": "2.1.0",
- "json5": "1.0.1"
- }
- },
- "locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "dev": true,
- "requires": {
- "p-locate": "3.0.0",
- "path-exists": "3.0.0"
- }
- },
- "lodash": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
- "dev": true
- },
- "lodash._reinterpolate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
- "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
- "dev": true
- },
- "lodash.memoize": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
- "dev": true
- },
- "lodash.tail": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
- "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=",
- "dev": true
- },
- "lodash.template": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",
- "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=",
- "dev": true,
- "requires": {
- "lodash._reinterpolate": "3.0.0",
- "lodash.templatesettings": "4.1.0"
- }
- },
- "lodash.templatesettings": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz",
- "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=",
- "dev": true,
- "requires": {
- "lodash._reinterpolate": "3.0.0"
- }
- },
- "lodash.uniq": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
- "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
- "dev": true
- },
- "loud-rejection": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
- "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
- "dev": true,
- "requires": {
- "currently-unhandled": "0.4.1",
- "signal-exit": "3.0.2"
- }
- },
- "lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "requires": {
- "yallist": "3.0.3"
- }
- },
- "make-dir": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
- "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
- "dev": true,
- "requires": {
- "pify": "4.0.1",
- "semver": "5.7.0"
- },
- "dependencies": {
- "pify": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
- "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
- "dev": true
- }
- }
- },
- "mamacro": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
- "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==",
- "dev": true
- },
- "map-age-cleaner": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
- "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
- "dev": true,
- "requires": {
- "p-defer": "1.0.0"
- }
- },
- "map-cache": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
- "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
- "dev": true
- },
- "map-obj": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
- "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
- "dev": true
- },
- "map-visit": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
- "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
- "dev": true,
- "requires": {
- "object-visit": "1.0.1"
- }
- },
- "materialize-css": {
- "version": "1.0.0-rc.2",
- "resolved": "https://registry.npmjs.org/materialize-css/-/materialize-css-1.0.0-rc.2.tgz",
- "integrity": "sha512-FuQmSyq4Qv0ov7A2qXw0E6/jbQzSWx2P1pg2/XQDYTkkSc/GyiFAxu3fw9zgShwuTvyumEiw5jkxQWT9siqMBQ==",
- "dev": true
- },
- "md5.js": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
- "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
- "dev": true,
- "requires": {
- "hash-base": "3.0.4",
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "mdn-data": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz",
- "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
- "dev": true
- },
- "mem": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
- "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
- "dev": true,
- "requires": {
- "map-age-cleaner": "0.1.3",
- "mimic-fn": "2.1.0",
- "p-is-promise": "2.1.0"
- }
- },
- "memory-fs": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
- "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
- "dev": true,
- "requires": {
- "errno": "0.1.7",
- "readable-stream": "2.3.6"
- }
- },
- "meow": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
- "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
- "dev": true,
- "requires": {
- "camelcase-keys": "2.1.0",
- "decamelize": "1.2.0",
- "loud-rejection": "1.6.0",
- "map-obj": "1.0.1",
- "minimist": "1.2.0",
- "normalize-package-data": "2.5.0",
- "object-assign": "4.1.1",
- "read-pkg-up": "1.0.1",
- "redent": "1.0.0",
- "trim-newlines": "1.0.0"
- }
- },
- "micromatch": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
- "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
- "dev": true,
- "requires": {
- "arr-diff": "4.0.0",
- "array-unique": "0.3.2",
- "braces": "2.3.2",
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "extglob": "2.0.4",
- "fragment-cache": "0.2.1",
- "kind-of": "6.0.2",
- "nanomatch": "1.2.13",
- "object.pick": "1.3.0",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- }
- },
- "miller-rabin": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
- "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "brorand": "1.1.0"
- }
- },
- "mime-db": {
- "version": "1.40.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
- "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
- "dev": true
- },
- "mime-types": {
- "version": "2.1.24",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
- "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
- "dev": true,
- "requires": {
- "mime-db": "1.40.0"
- }
- },
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true
- },
- "mini-css-extract-plugin": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz",
- "integrity": "sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==",
- "dev": true,
- "requires": {
- "loader-utils": "1.2.3",
- "normalize-url": "2.0.1",
- "schema-utils": "1.0.0",
- "webpack-sources": "1.3.0"
- },
- "dependencies": {
- "normalize-url": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz",
- "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==",
- "dev": true,
- "requires": {
- "prepend-http": "2.0.0",
- "query-string": "5.1.1",
- "sort-keys": "2.0.0"
- }
- }
- }
- },
- "minimalistic-assert": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
- "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
- "dev": true
- },
- "minimalistic-crypto-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
- "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
- "dev": true
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "requires": {
- "brace-expansion": "1.1.11"
- }
- },
- "minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
- },
- "mississippi": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
- "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
- "dev": true,
- "requires": {
- "concat-stream": "1.6.2",
- "duplexify": "3.7.1",
- "end-of-stream": "1.4.1",
- "flush-write-stream": "1.1.1",
- "from2": "2.3.0",
- "parallel-transform": "1.1.0",
- "pump": "3.0.0",
- "pumpify": "1.5.1",
- "stream-each": "1.2.3",
- "through2": "2.0.5"
- }
- },
- "mixin-deep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
- "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
- "dev": true,
- "requires": {
- "for-in": "1.0.2",
- "is-extendable": "1.0.1"
- },
- "dependencies": {
- "is-extendable": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
- "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
- "dev": true,
- "requires": {
- "is-plain-object": "2.0.4"
- }
- }
- }
- },
- "mixin-object": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
- "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
- "dev": true,
- "requires": {
- "for-in": "0.1.8",
- "is-extendable": "0.1.1"
- },
- "dependencies": {
- "for-in": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
- "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=",
- "dev": true
- }
- }
- },
- "mkdirp": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
- "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
- "dev": true,
- "requires": {
- "minimist": "0.0.8"
- },
- "dependencies": {
- "minimist": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true
- }
- }
- },
- "move-concurrently": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
- "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=",
- "dev": true,
- "requires": {
- "aproba": "1.2.0",
- "copy-concurrently": "1.0.5",
- "fs-write-stream-atomic": "1.0.10",
- "mkdirp": "0.5.1",
- "rimraf": "2.6.3",
- "run-queue": "1.0.3"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
- "nan": {
- "version": "2.13.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
- "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
- "dev": true
- },
- "nanomatch": {
- "version": "1.2.13",
- "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
- "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
- "dev": true,
- "requires": {
- "arr-diff": "4.0.0",
- "array-unique": "0.3.2",
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "fragment-cache": "0.2.1",
- "is-windows": "1.0.2",
- "kind-of": "6.0.2",
- "object.pick": "1.3.0",
- "regex-not": "1.0.2",
- "snapdragon": "0.8.2",
- "to-regex": "3.0.2"
- }
- },
- "neo-async": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
- "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
- "dev": true
- },
- "nice-try": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
- "dev": true
- },
- "node-gyp": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
- "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
- "dev": true,
- "requires": {
- "fstream": "1.0.12",
- "glob": "7.1.4",
- "graceful-fs": "4.1.15",
- "mkdirp": "0.5.1",
- "nopt": "3.0.6",
- "npmlog": "4.1.2",
- "osenv": "0.1.5",
- "request": "2.88.0",
- "rimraf": "2.6.3",
- "semver": "5.3.0",
- "tar": "2.2.2",
- "which": "1.3.1"
- },
- "dependencies": {
- "semver": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
- "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
- "dev": true
- }
- }
- },
- "node-libs-browser": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz",
- "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==",
- "dev": true,
- "requires": {
- "assert": "1.5.0",
- "browserify-zlib": "0.2.0",
- "buffer": "4.9.1",
- "console-browserify": "1.1.0",
- "constants-browserify": "1.0.0",
- "crypto-browserify": "3.12.0",
- "domain-browser": "1.2.0",
- "events": "3.0.0",
- "https-browserify": "1.0.0",
- "os-browserify": "0.3.0",
- "path-browserify": "0.0.0",
- "process": "0.11.10",
- "punycode": "1.4.1",
- "querystring-es3": "0.2.1",
- "readable-stream": "2.3.6",
- "stream-browserify": "2.0.2",
- "stream-http": "2.8.3",
- "string_decoder": "1.1.1",
- "timers-browserify": "2.0.10",
- "tty-browserify": "0.0.0",
- "url": "0.11.0",
- "util": "0.11.1",
- "vm-browserify": "0.0.4"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
- }
- }
- },
- "node-releases": {
- "version": "1.1.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.19.tgz",
- "integrity": "sha512-SH/B4WwovHbulIALsQllAVwqZZD1kPmKCqrhGfR29dXjLAVZMHvBjD3S6nL9D/J9QkmZ1R92/0wCMDKXUUvyyA==",
- "dev": true,
- "requires": {
- "semver": "5.7.0"
- }
- },
- "node-sass": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
- "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
- "dev": true,
- "requires": {
- "async-foreach": "0.1.3",
- "chalk": "1.1.3",
- "cross-spawn": "3.0.1",
- "gaze": "1.1.3",
- "get-stdin": "4.0.1",
- "glob": "7.1.4",
- "in-publish": "2.0.0",
- "lodash": "4.17.11",
- "meow": "3.7.0",
- "mkdirp": "0.5.1",
- "nan": "2.13.2",
- "node-gyp": "3.8.0",
- "npmlog": "4.1.2",
- "request": "2.88.0",
- "sass-graph": "2.2.4",
- "stdout-stream": "1.4.1",
- "true-case-path": "1.0.3"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
- "requires": {
- "ansi-styles": "2.2.1",
- "escape-string-regexp": "1.0.5",
- "has-ansi": "2.0.0",
- "strip-ansi": "3.0.1",
- "supports-color": "2.0.0"
- }
- },
- "cross-spawn": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
- "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
- "dev": true,
- "requires": {
- "lru-cache": "4.1.5",
- "which": "1.3.1"
- }
- },
- "lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
- "dev": true,
- "requires": {
- "pseudomap": "1.0.2",
- "yallist": "2.1.2"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "2.1.1"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
- },
- "yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
- }
- }
- },
- "nopt": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
- "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
- "dev": true,
- "requires": {
- "abbrev": "1.1.1"
- }
- },
- "normalize-package-data": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
- "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
- "dev": true,
- "requires": {
- "hosted-git-info": "2.7.1",
- "resolve": "1.11.0",
- "semver": "5.7.0",
- "validate-npm-package-license": "3.0.4"
- }
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
- },
- "normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
- "dev": true
- },
- "normalize-url": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
- "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
- "dev": true
- },
- "npm-run-path": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
- "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
- "dev": true,
- "requires": {
- "path-key": "2.0.1"
- }
- },
- "npmlog": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
- "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "dev": true,
- "requires": {
- "are-we-there-yet": "1.1.5",
- "console-control-strings": "1.1.0",
- "gauge": "2.7.4",
- "set-blocking": "2.0.0"
- }
- },
- "nth-check": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
- "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
- "dev": true,
- "requires": {
- "boolbase": "1.0.0"
- }
- },
- "num2fraction": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
- "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
- "dev": true
- },
- "number-is-nan": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
- },
- "oauth-sign": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
- "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
- "dev": true
- },
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
- },
- "object-copy": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
- "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
- "dev": true,
- "requires": {
- "copy-descriptor": "0.1.1",
- "define-property": "0.2.5",
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true
- },
- "object-visit": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
- "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
- "dev": true,
- "requires": {
- "isobject": "3.0.1"
- }
- },
- "object.getownpropertydescriptors": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
- "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
- "dev": true,
- "requires": {
- "define-properties": "1.1.3",
- "es-abstract": "1.13.0"
- }
- },
- "object.pick": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
- "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
- "dev": true,
- "requires": {
- "isobject": "3.0.1"
- }
- },
- "object.values": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz",
- "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==",
- "dev": true,
- "requires": {
- "define-properties": "1.1.3",
- "es-abstract": "1.13.0",
- "function-bind": "1.1.1",
- "has": "1.0.3"
- }
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "requires": {
- "wrappy": "1.0.2"
- }
- },
- "os-browserify": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
- "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
- "dev": true
- },
- "os-homedir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
- "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
- "dev": true
- },
- "os-locale": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
- "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
- "dev": true,
- "requires": {
- "execa": "1.0.0",
- "lcid": "2.0.0",
- "mem": "4.3.0"
- }
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
- },
- "osenv": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
- "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
- "dev": true,
- "requires": {
- "os-homedir": "1.0.2",
- "os-tmpdir": "1.0.2"
- }
- },
- "p-defer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
- "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
- "dev": true
- },
- "p-finally": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
- },
- "p-is-promise": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
- "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
- "dev": true
- },
- "p-limit": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
- "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
- "dev": true,
- "requires": {
- "p-try": "2.2.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dev": true,
- "requires": {
- "p-limit": "2.2.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true
- },
- "pako": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
- "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==",
- "dev": true
- },
- "parallel-transform": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz",
- "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=",
- "dev": true,
- "requires": {
- "cyclist": "0.2.2",
- "inherits": "2.0.3",
- "readable-stream": "2.3.6"
- }
- },
- "parse-asn1": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
- "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
- "dev": true,
- "requires": {
- "asn1.js": "4.10.1",
- "browserify-aes": "1.2.0",
- "create-hash": "1.2.0",
- "evp_bytestokey": "1.0.3",
- "pbkdf2": "3.0.17",
- "safe-buffer": "5.1.2"
- }
- },
- "parse-json": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
- "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
- "dev": true,
- "requires": {
- "error-ex": "1.3.2",
- "json-parse-better-errors": "1.0.2"
- }
- },
- "parse-passwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
- "dev": true
- },
- "pascalcase": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
- "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
- "dev": true
- },
- "path-browserify": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
- "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
- "dev": true
- },
- "path-dirname": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
- "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
- "dev": true
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "path-key": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
- "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
- "dev": true
- },
- "path-parse": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
- "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
- "dev": true
- },
- "path-type": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
- "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "pify": "2.3.0",
- "pinkie-promise": "2.0.1"
- },
- "dependencies": {
- "pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
- }
- }
- },
- "pbkdf2": {
- "version": "3.0.17",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
- "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
- "dev": true,
- "requires": {
- "create-hash": "1.2.0",
- "create-hmac": "1.1.7",
- "ripemd160": "2.0.2",
- "safe-buffer": "5.1.2",
- "sha.js": "2.4.11"
- }
- },
- "performance-now": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
- "dev": true
- },
- "pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
- "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
- },
- "pinkie": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
- "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
- "dev": true
- },
- "pinkie-promise": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
- "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
- "dev": true,
- "requires": {
- "pinkie": "2.0.4"
- }
- },
- "pkg-dir": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
- "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
- "dev": true,
- "requires": {
- "find-up": "3.0.0"
- }
- },
- "posix-character-classes": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
- "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
- "dev": true
- },
- "postcss": {
- "version": "7.0.16",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.16.tgz",
- "integrity": "sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA==",
- "dev": true,
- "requires": {
- "chalk": "2.4.2",
- "source-map": "0.6.1",
- "supports-color": "6.1.0"
- }
- },
- "postcss-attribute-case-insensitive": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz",
- "integrity": "sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0"
- }
- },
- "postcss-calc": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz",
- "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==",
- "dev": true,
- "requires": {
- "css-unit-converter": "1.1.1",
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-color-functional-notation": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz",
- "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-color-gray": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz",
- "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==",
- "dev": true,
- "requires": {
- "@csstools/convert-colors": "1.4.0",
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-color-hex-alpha": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz",
- "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-color-mod-function": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz",
- "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==",
- "dev": true,
- "requires": {
- "@csstools/convert-colors": "1.4.0",
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-color-rebeccapurple": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz",
- "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-colormin": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz",
- "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "color": "3.1.1",
- "has": "1.0.3",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-convert-values": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz",
- "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-custom-media": {
- "version": "7.0.8",
- "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz",
- "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-custom-properties": {
- "version": "8.0.10",
- "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.10.tgz",
- "integrity": "sha512-GDL0dyd7++goDR4SSasYdRNNvp4Gqy1XMzcCnTijiph7VB27XXpJ8bW/AI0i2VSBZ55TpdGhMr37kMSpRfYD0Q==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-custom-selectors": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz",
- "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0"
- }
- },
- "postcss-dir-pseudo-class": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz",
- "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0"
- }
- },
- "postcss-discard-comments": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz",
- "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-discard-duplicates": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz",
- "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-discard-empty": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz",
- "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-discard-overridden": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz",
- "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-double-position-gradients": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz",
- "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-env-function": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz",
- "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-focus-visible": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz",
- "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-focus-within": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz",
- "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-font-variant": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz",
- "integrity": "sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-gap-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz",
- "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-image-set-function": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz",
- "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-initial": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.0.tgz",
- "integrity": "sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q==",
- "dev": true,
- "requires": {
- "lodash.template": "4.4.0",
- "postcss": "7.0.16"
- }
- },
- "postcss-lab-function": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz",
- "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==",
- "dev": true,
- "requires": {
- "@csstools/convert-colors": "1.4.0",
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-load-config": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz",
- "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==",
- "dev": true,
- "requires": {
- "cosmiconfig": "4.0.0",
- "import-cwd": "2.1.0"
- }
- },
- "postcss-loader": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz",
- "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==",
- "dev": true,
- "requires": {
- "loader-utils": "1.2.3",
- "postcss": "7.0.16",
- "postcss-load-config": "2.0.0",
- "schema-utils": "1.0.0"
- }
- },
- "postcss-logical": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz",
- "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-media-minmax": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz",
- "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-merge-longhand": {
- "version": "4.0.11",
- "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz",
- "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==",
- "dev": true,
- "requires": {
- "css-color-names": "0.0.4",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1",
- "stylehacks": "4.0.3"
- }
- },
- "postcss-merge-rules": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz",
- "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "caniuse-api": "3.0.0",
- "cssnano-util-same-parent": "4.0.1",
- "postcss": "7.0.16",
- "postcss-selector-parser": "3.1.1",
- "vendors": "1.0.3"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
- "dev": true,
- "requires": {
- "dot-prop": "4.2.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- }
- }
- },
- "postcss-minify-font-values": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz",
- "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-minify-gradients": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz",
- "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "4.0.0",
- "is-color-stop": "1.1.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-minify-params": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz",
- "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==",
- "dev": true,
- "requires": {
- "alphanum-sort": "1.0.2",
- "browserslist": "4.6.0",
- "cssnano-util-get-arguments": "4.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1",
- "uniqs": "2.0.0"
- }
- },
- "postcss-minify-selectors": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz",
- "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==",
- "dev": true,
- "requires": {
- "alphanum-sort": "1.0.2",
- "has": "1.0.3",
- "postcss": "7.0.16",
- "postcss-selector-parser": "3.1.1"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
- "dev": true,
- "requires": {
- "dot-prop": "4.2.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- }
- }
- },
- "postcss-modules-extract-imports": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
- "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-modules-local-by-default": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz",
- "integrity": "sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "6.0.2",
- "postcss-value-parser": "3.3.1"
- },
- "dependencies": {
- "cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true
- },
- "postcss-selector-parser": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
- "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
- "dev": true,
- "requires": {
- "cssesc": "3.0.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- }
- }
- },
- "postcss-modules-scope": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz",
- "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "6.0.2"
- },
- "dependencies": {
- "cssesc": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true
- },
- "postcss-selector-parser": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
- "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
- "dev": true,
- "requires": {
- "cssesc": "3.0.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- }
- }
- },
- "postcss-modules-values": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz",
- "integrity": "sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==",
- "dev": true,
- "requires": {
- "icss-replace-symbols": "1.1.0",
- "postcss": "7.0.16"
- }
- },
- "postcss-nesting": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.0.tgz",
- "integrity": "sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-normalize-charset": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz",
- "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-normalize-display-values": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz",
- "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "4.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-positions": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz",
- "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "4.0.0",
- "has": "1.0.3",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-repeat-style": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz",
- "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "4.0.0",
- "cssnano-util-get-match": "4.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-string": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz",
- "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==",
- "dev": true,
- "requires": {
- "has": "1.0.3",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-timing-functions": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz",
- "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "4.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-unicode": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz",
- "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-url": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz",
- "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==",
- "dev": true,
- "requires": {
- "is-absolute-url": "2.1.0",
- "normalize-url": "3.3.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-normalize-whitespace": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz",
- "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-ordered-values": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz",
- "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==",
- "dev": true,
- "requires": {
- "cssnano-util-get-arguments": "4.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-overflow-shorthand": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz",
- "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-page-break": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz",
- "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-place": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz",
- "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-values-parser": "2.0.1"
- }
- },
- "postcss-preset-env": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz",
- "integrity": "sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA==",
- "dev": true,
- "requires": {
- "autoprefixer": "9.5.1",
- "browserslist": "4.6.0",
- "caniuse-lite": "1.0.30000969",
- "css-blank-pseudo": "0.1.4",
- "css-has-pseudo": "0.10.0",
- "css-prefers-color-scheme": "3.1.1",
- "cssdb": "4.4.0",
- "postcss": "7.0.16",
- "postcss-attribute-case-insensitive": "4.0.1",
- "postcss-color-functional-notation": "2.0.1",
- "postcss-color-gray": "5.0.0",
- "postcss-color-hex-alpha": "5.0.3",
- "postcss-color-mod-function": "3.0.3",
- "postcss-color-rebeccapurple": "4.0.1",
- "postcss-custom-media": "7.0.8",
- "postcss-custom-properties": "8.0.10",
- "postcss-custom-selectors": "5.1.2",
- "postcss-dir-pseudo-class": "5.0.0",
- "postcss-double-position-gradients": "1.0.0",
- "postcss-env-function": "2.0.2",
- "postcss-focus-visible": "4.0.0",
- "postcss-focus-within": "3.0.0",
- "postcss-font-variant": "4.0.0",
- "postcss-gap-properties": "2.0.0",
- "postcss-image-set-function": "3.0.1",
- "postcss-initial": "3.0.0",
- "postcss-lab-function": "2.0.1",
- "postcss-logical": "3.0.0",
- "postcss-media-minmax": "4.0.0",
- "postcss-nesting": "7.0.0",
- "postcss-overflow-shorthand": "2.0.0",
- "postcss-page-break": "2.0.0",
- "postcss-place": "4.0.1",
- "postcss-pseudo-class-any-link": "6.0.0",
- "postcss-replace-overflow-wrap": "3.0.0",
- "postcss-selector-matches": "4.0.0",
- "postcss-selector-not": "4.0.0"
- }
- },
- "postcss-pseudo-class-any-link": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz",
- "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16",
- "postcss-selector-parser": "5.0.0"
- }
- },
- "postcss-reduce-initial": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz",
- "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "caniuse-api": "3.0.0",
- "has": "1.0.3",
- "postcss": "7.0.16"
- }
- },
- "postcss-reduce-transforms": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz",
- "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==",
- "dev": true,
- "requires": {
- "cssnano-util-get-match": "4.0.0",
- "has": "1.0.3",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1"
- }
- },
- "postcss-replace-overflow-wrap": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz",
- "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-scss": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.0.0.tgz",
- "integrity": "sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug==",
- "dev": true,
- "requires": {
- "postcss": "7.0.16"
- }
- },
- "postcss-selector-matches": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz",
- "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==",
- "dev": true,
- "requires": {
- "balanced-match": "1.0.0",
- "postcss": "7.0.16"
- }
- },
- "postcss-selector-not": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz",
- "integrity": "sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==",
- "dev": true,
- "requires": {
- "balanced-match": "1.0.0",
- "postcss": "7.0.16"
- }
- },
- "postcss-selector-parser": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
- "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
- "dev": true,
- "requires": {
- "cssesc": "2.0.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- },
- "postcss-svgo": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz",
- "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==",
- "dev": true,
- "requires": {
- "is-svg": "3.0.0",
- "postcss": "7.0.16",
- "postcss-value-parser": "3.3.1",
- "svgo": "1.2.2"
- }
- },
- "postcss-unique-selectors": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz",
- "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==",
- "dev": true,
- "requires": {
- "alphanum-sort": "1.0.2",
- "postcss": "7.0.16",
- "uniqs": "2.0.0"
- }
- },
- "postcss-value-parser": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
- "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
- "dev": true
- },
- "postcss-values-parser": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz",
- "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==",
- "dev": true,
- "requires": {
- "flatten": "1.0.2",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- },
- "prepend-http": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
- "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
- "dev": true
- },
- "process": {
- "version": "0.11.10",
- "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
- "dev": true
- },
- "process-nextick-args": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
- "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
- "dev": true
- },
- "promise-inflight": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
- "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
- "dev": true
- },
- "prr": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
- "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
- "dev": true
- },
- "pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
- "dev": true
- },
- "psl": {
- "version": "1.1.31",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
- "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==",
- "dev": true
- },
- "public-encrypt": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
- "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
- "dev": true,
- "requires": {
- "bn.js": "4.11.8",
- "browserify-rsa": "4.0.1",
- "create-hash": "1.2.0",
- "parse-asn1": "5.1.4",
- "randombytes": "2.1.0",
- "safe-buffer": "5.1.2"
- }
- },
- "pump": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
- "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
- "dev": true,
- "requires": {
- "end-of-stream": "1.4.1",
- "once": "1.4.0"
- }
- },
- "pumpify": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
- "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
- "dev": true,
- "requires": {
- "duplexify": "3.7.1",
- "inherits": "2.0.3",
- "pump": "2.0.1"
- },
- "dependencies": {
- "pump": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
- "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
- "dev": true,
- "requires": {
- "end-of-stream": "1.4.1",
- "once": "1.4.0"
- }
- }
- }
- },
- "punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true
- },
- "q": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
- "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
- "dev": true
- },
- "qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
- "dev": true
- },
- "query-string": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
- "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
- "dev": true,
- "requires": {
- "decode-uri-component": "0.2.0",
- "object-assign": "4.1.1",
- "strict-uri-encode": "1.1.0"
- }
- },
- "querystring": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
- "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
- "dev": true
- },
- "querystring-es3": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
- "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
- "dev": true
- },
- "randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
- "requires": {
- "safe-buffer": "5.1.2"
- }
- },
- "randomfill": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
- "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
- "dev": true,
- "requires": {
- "randombytes": "2.1.0",
- "safe-buffer": "5.1.2"
- }
- },
- "read-pkg": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
- "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
- "dev": true,
- "requires": {
- "load-json-file": "1.1.0",
- "normalize-package-data": "2.5.0",
- "path-type": "1.1.0"
- }
- },
- "read-pkg-up": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
- "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
- "dev": true,
- "requires": {
- "find-up": "1.1.2",
- "read-pkg": "1.1.0"
- },
- "dependencies": {
- "find-up": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
- "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
- "dev": true,
- "requires": {
- "path-exists": "2.1.0",
- "pinkie-promise": "2.0.1"
- }
- },
- "path-exists": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
- "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
- "dev": true,
- "requires": {
- "pinkie-promise": "2.0.1"
- }
- }
- }
- },
- "readable-stream": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
- "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
- "dev": true,
- "requires": {
- "core-util-is": "1.0.2",
- "inherits": "2.0.3",
- "isarray": "1.0.0",
- "process-nextick-args": "2.0.0",
- "safe-buffer": "5.1.2",
- "string_decoder": "1.1.1",
- "util-deprecate": "1.0.2"
- }
- },
- "readdirp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
- "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
- "dev": true,
- "requires": {
- "graceful-fs": "4.1.15",
- "micromatch": "3.1.10",
- "readable-stream": "2.3.6"
- }
- },
- "redent": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
- "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
- "dev": true,
- "requires": {
- "indent-string": "2.1.0",
- "strip-indent": "1.0.1"
- }
- },
- "regex-not": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
- "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
- "dev": true,
- "requires": {
- "extend-shallow": "3.0.2",
- "safe-regex": "1.1.0"
- }
- },
- "remove-trailing-separator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
- "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
- "dev": true
- },
- "repeat-element": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
- "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
- "dev": true
- },
- "repeat-string": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
- "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
- "dev": true
- },
- "repeating": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
- "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
- "dev": true,
- "requires": {
- "is-finite": "1.0.2"
- }
- },
- "request": {
- "version": "2.88.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
- "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
- "dev": true,
- "requires": {
- "aws-sign2": "0.7.0",
- "aws4": "1.8.0",
- "caseless": "0.12.0",
- "combined-stream": "1.0.8",
- "extend": "3.0.2",
- "forever-agent": "0.6.1",
- "form-data": "2.3.3",
- "har-validator": "5.1.3",
- "http-signature": "1.2.0",
- "is-typedarray": "1.0.0",
- "isstream": "0.1.2",
- "json-stringify-safe": "5.0.1",
- "mime-types": "2.1.24",
- "oauth-sign": "0.9.0",
- "performance-now": "2.1.0",
- "qs": "6.5.2",
- "safe-buffer": "5.1.2",
- "tough-cookie": "2.4.3",
- "tunnel-agent": "0.6.0",
- "uuid": "3.3.2"
- }
- },
- "require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
- },
- "require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true
- },
- "require-main-filename": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
- "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
- "dev": true
- },
- "resolve": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
- "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==",
- "dev": true,
- "requires": {
- "path-parse": "1.0.6"
- }
- },
- "resolve-cwd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz",
- "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=",
- "dev": true,
- "requires": {
- "resolve-from": "3.0.0"
- }
- },
- "resolve-dir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
- "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=",
- "dev": true,
- "requires": {
- "expand-tilde": "2.0.2",
- "global-modules": "1.0.0"
- }
- },
- "resolve-from": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
- "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
- "dev": true
- },
- "resolve-url": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
- "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
- "dev": true
- },
- "ret": {
- "version": "0.1.15",
- "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
- "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
- "dev": true
- },
- "rgb-regex": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
- "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=",
- "dev": true
- },
- "rgba-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
- "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
- "dev": true
- },
- "rimraf": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
- "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
- "dev": true,
- "requires": {
- "glob": "7.1.4"
- }
- },
- "ripemd160": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
- "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
- "dev": true,
- "requires": {
- "hash-base": "3.0.4",
- "inherits": "2.0.3"
- }
- },
- "run-queue": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
- "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=",
- "dev": true,
- "requires": {
- "aproba": "1.2.0"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
- },
- "safe-regex": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
- "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
- "dev": true,
- "requires": {
- "ret": "0.1.15"
- }
- },
- "safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
- },
- "sass-graph": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
- "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
- "dev": true,
- "requires": {
- "glob": "7.1.4",
- "lodash": "4.17.11",
- "scss-tokenizer": "0.2.3",
- "yargs": "7.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "camelcase": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
- "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
- "dev": true
- },
- "cliui": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
- "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
- "dev": true,
- "requires": {
- "string-width": "1.0.2",
- "strip-ansi": "3.0.1",
- "wrap-ansi": "2.1.0"
- }
- },
- "invert-kv": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
- "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
- "requires": {
- "number-is-nan": "1.0.1"
- }
- },
- "lcid": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
- "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
- "dev": true,
- "requires": {
- "invert-kv": "1.0.0"
- }
- },
- "os-locale": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
- "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
- "dev": true,
- "requires": {
- "lcid": "1.0.0"
- }
- },
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
- "requires": {
- "code-point-at": "1.1.0",
- "is-fullwidth-code-point": "1.0.0",
- "strip-ansi": "3.0.1"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "2.1.1"
- }
- },
- "which-module": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
- "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
- "dev": true
- },
- "y18n": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
- "dev": true
- },
- "yargs": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
- "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
- "dev": true,
- "requires": {
- "camelcase": "3.0.0",
- "cliui": "3.2.0",
- "decamelize": "1.2.0",
- "get-caller-file": "1.0.3",
- "os-locale": "1.4.0",
- "read-pkg-up": "1.0.1",
- "require-directory": "2.1.1",
- "require-main-filename": "1.0.1",
- "set-blocking": "2.0.0",
- "string-width": "1.0.2",
- "which-module": "1.0.0",
- "y18n": "3.2.1",
- "yargs-parser": "5.0.0"
- }
- },
- "yargs-parser": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
- "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
- "dev": true,
- "requires": {
- "camelcase": "3.0.0"
- }
- }
- }
- },
- "sass-loader": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz",
- "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==",
- "dev": true,
- "requires": {
- "clone-deep": "2.0.2",
- "loader-utils": "1.2.3",
- "lodash.tail": "4.1.1",
- "neo-async": "2.6.1",
- "pify": "3.0.0",
- "semver": "5.7.0"
- }
- },
- "sax": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
- "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
- "dev": true
- },
- "schema-utils": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
- "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
- "dev": true,
- "requires": {
- "ajv": "6.10.0",
- "ajv-errors": "1.0.1",
- "ajv-keywords": "3.4.0"
- }
- },
- "scss-tokenizer": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
- "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
- "dev": true,
- "requires": {
- "js-base64": "2.5.1",
- "source-map": "0.4.4"
- },
- "dependencies": {
- "source-map": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
- "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
- "dev": true,
- "requires": {
- "amdefine": "1.0.1"
- }
- }
- }
- },
- "semver": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
- "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
- "dev": true
- },
- "serialize-javascript": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz",
- "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==",
- "dev": true
- },
- "set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "dev": true
- },
- "set-value": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
- "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-extendable": "0.1.1",
- "is-plain-object": "2.0.4",
- "split-string": "3.1.0"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- }
- }
- },
- "setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
- "dev": true
- },
- "sha.js": {
- "version": "2.4.11",
- "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
- "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "safe-buffer": "5.1.2"
- }
- },
- "shallow-clone": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
- "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1",
- "kind-of": "5.1.0",
- "mixin-object": "2.0.1"
- },
- "dependencies": {
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
- }
- },
- "shebang-command": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
- "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
- "dev": true,
- "requires": {
- "shebang-regex": "1.0.0"
- }
- },
- "shebang-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
- "dev": true
- },
- "signal-exit": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
- "dev": true
- },
- "simple-swizzle": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
- "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
- "dev": true,
- "requires": {
- "is-arrayish": "0.3.2"
- },
- "dependencies": {
- "is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
- "dev": true
- }
- }
- },
- "snapdragon": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
- "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
- "dev": true,
- "requires": {
- "base": "0.11.2",
- "debug": "2.6.9",
- "define-property": "0.2.5",
- "extend-shallow": "2.0.1",
- "map-cache": "0.2.2",
- "source-map": "0.5.7",
- "source-map-resolve": "0.5.2",
- "use": "3.1.1"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
- "dev": true
- }
- }
- },
- "snapdragon-node": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
- "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
- "dev": true,
- "requires": {
- "define-property": "1.0.0",
- "isobject": "3.0.1",
- "snapdragon-util": "3.0.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
- "dev": true,
- "requires": {
- "is-descriptor": "1.0.2"
- }
- },
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
- "requires": {
- "kind-of": "6.0.2"
- }
- },
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
- "requires": {
- "is-accessor-descriptor": "1.0.0",
- "is-data-descriptor": "1.0.0",
- "kind-of": "6.0.2"
- }
- }
- }
- },
- "snapdragon-util": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
- "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "sort-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz",
- "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=",
- "dev": true,
- "requires": {
- "is-plain-obj": "1.1.0"
- }
- },
- "source-list-map": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
- "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
- "dev": true
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
- },
- "source-map-resolve": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
- "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
- "dev": true,
- "requires": {
- "atob": "2.1.2",
- "decode-uri-component": "0.2.0",
- "resolve-url": "0.2.1",
- "source-map-url": "0.4.0",
- "urix": "0.1.0"
- }
- },
- "source-map-support": {
- "version": "0.5.12",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
- "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
- "dev": true,
- "requires": {
- "buffer-from": "1.1.1",
- "source-map": "0.6.1"
- }
- },
- "source-map-url": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
- "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
- "dev": true
- },
- "spdx-correct": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
- "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
- "dev": true,
- "requires": {
- "spdx-expression-parse": "3.0.0",
- "spdx-license-ids": "3.0.4"
- }
- },
- "spdx-exceptions": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
- "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
- "dev": true
- },
- "spdx-expression-parse": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
- "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
- "dev": true,
- "requires": {
- "spdx-exceptions": "2.2.0",
- "spdx-license-ids": "3.0.4"
- }
- },
- "spdx-license-ids": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
- "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==",
- "dev": true
- },
- "split-string": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
- "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
- "dev": true,
- "requires": {
- "extend-shallow": "3.0.2"
- }
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
- "dev": true
- },
- "sshpk": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
- "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
- "dev": true,
- "requires": {
- "asn1": "0.2.4",
- "assert-plus": "1.0.0",
- "bcrypt-pbkdf": "1.0.2",
- "dashdash": "1.14.1",
- "ecc-jsbn": "0.1.2",
- "getpass": "0.1.7",
- "jsbn": "0.1.1",
- "safer-buffer": "2.1.2",
- "tweetnacl": "0.14.5"
- }
- },
- "ssri": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
- "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
- "dev": true,
- "requires": {
- "figgy-pudding": "3.5.1"
- }
- },
- "stable": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
- "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
- "dev": true
- },
- "static-extend": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
- "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
- "dev": true,
- "requires": {
- "define-property": "0.2.5",
- "object-copy": "0.1.0"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
- "dev": true,
- "requires": {
- "is-descriptor": "0.1.6"
- }
- }
- }
- },
- "stdout-stream": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
- "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
- "dev": true,
- "requires": {
- "readable-stream": "2.3.6"
- }
- },
- "stream-browserify": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
- "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3",
- "readable-stream": "2.3.6"
- }
- },
- "stream-each": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
- "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==",
- "dev": true,
- "requires": {
- "end-of-stream": "1.4.1",
- "stream-shift": "1.0.0"
- }
- },
- "stream-http": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
- "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
- "dev": true,
- "requires": {
- "builtin-status-codes": "3.0.0",
- "inherits": "2.0.3",
- "readable-stream": "2.3.6",
- "to-arraybuffer": "1.0.1",
- "xtend": "4.0.1"
- }
- },
- "stream-shift": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
- "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
- "dev": true
- },
- "strict-uri-encode": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
- "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "2.0.0",
- "strip-ansi": "4.0.0"
- }
- },
- "string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
- "requires": {
- "safe-buffer": "5.1.2"
- }
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "3.0.0"
- }
- },
- "strip-bom": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
- "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
- "dev": true,
- "requires": {
- "is-utf8": "0.2.1"
- }
- },
- "strip-eof": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
- "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
- "dev": true
- },
- "strip-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
- "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
- "dev": true,
- "requires": {
- "get-stdin": "4.0.1"
- }
- },
- "stylehacks": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
- "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==",
- "dev": true,
- "requires": {
- "browserslist": "4.6.0",
- "postcss": "7.0.16",
- "postcss-selector-parser": "3.1.1"
- },
- "dependencies": {
- "postcss-selector-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz",
- "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=",
- "dev": true,
- "requires": {
- "dot-prop": "4.2.0",
- "indexes-of": "1.0.1",
- "uniq": "1.0.1"
- }
- }
- }
- },
- "supports-color": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
- "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
- }
- },
- "svgo": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.2.2.tgz",
- "integrity": "sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==",
- "dev": true,
- "requires": {
- "chalk": "2.4.2",
- "coa": "2.0.2",
- "css-select": "2.0.2",
- "css-select-base-adapter": "0.1.1",
- "css-tree": "1.0.0-alpha.28",
- "css-url-regex": "1.1.0",
- "csso": "3.5.1",
- "js-yaml": "3.13.1",
- "mkdirp": "0.5.1",
- "object.values": "1.1.0",
- "sax": "1.2.4",
- "stable": "0.1.8",
- "unquote": "1.1.1",
- "util.promisify": "1.0.0"
- }
- },
- "tapable": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
- "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
- "dev": true
- },
- "tar": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
- "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
- "dev": true,
- "requires": {
- "block-stream": "0.0.9",
- "fstream": "1.0.12",
- "inherits": "2.0.3"
- }
- },
- "terser": {
- "version": "3.17.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz",
- "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==",
- "dev": true,
- "requires": {
- "commander": "2.20.0",
- "source-map": "0.6.1",
- "source-map-support": "0.5.12"
- }
- },
- "terser-webpack-plugin": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.4.tgz",
- "integrity": "sha512-64IiILNQlACWZLzFlpzNaG0bpQ4ytaB7fwOsbpsdIV70AfLUmIGGeuKL0YV2WmtcrURjE2aOvHD4/lrFV3Rg+Q==",
- "dev": true,
- "requires": {
- "cacache": "11.3.2",
- "find-cache-dir": "2.1.0",
- "is-wsl": "1.1.0",
- "schema-utils": "1.0.0",
- "serialize-javascript": "1.7.0",
- "source-map": "0.6.1",
- "terser": "3.17.0",
- "webpack-sources": "1.3.0",
- "worker-farm": "1.7.0"
- }
- },
- "through2": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
- "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
- "dev": true,
- "requires": {
- "readable-stream": "2.3.6",
- "xtend": "4.0.1"
- }
- },
- "timers-browserify": {
- "version": "2.0.10",
- "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
- "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==",
- "dev": true,
- "requires": {
- "setimmediate": "1.0.5"
- }
- },
- "timsort": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
- "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
- "dev": true
- },
- "to-arraybuffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
- "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
- "dev": true
- },
- "to-object-path": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
- "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
- "dev": true,
- "requires": {
- "kind-of": "3.2.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "1.1.6"
- }
- }
- }
- },
- "to-regex": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
- "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
- "dev": true,
- "requires": {
- "define-property": "2.0.2",
- "extend-shallow": "3.0.2",
- "regex-not": "1.0.2",
- "safe-regex": "1.1.0"
- }
- },
- "to-regex-range": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
- "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
- "dev": true,
- "requires": {
- "is-number": "3.0.0",
- "repeat-string": "1.6.1"
- }
- },
- "tough-cookie": {
- "version": "2.4.3",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
- "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
- "dev": true,
- "requires": {
- "psl": "1.1.31",
- "punycode": "1.4.1"
- },
- "dependencies": {
- "punycode": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
- }
- }
- },
- "trim-newlines": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
- "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
- "dev": true
- },
- "true-case-path": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
- "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
- "dev": true,
- "requires": {
- "glob": "7.1.4"
- }
- },
- "tslib": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
- "dev": true
- },
- "tty-browserify": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
- "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
- "dev": true
- },
- "tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
- "requires": {
- "safe-buffer": "5.1.2"
- }
- },
- "tweetnacl": {
- "version": "0.14.5",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true
- },
- "typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
- "dev": true
- },
- "union-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
- "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
- "dev": true,
- "requires": {
- "arr-union": "3.1.0",
- "get-value": "2.0.6",
- "is-extendable": "0.1.1",
- "set-value": "0.4.3"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "0.1.1"
- }
- },
- "set-value": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
- "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
- "dev": true,
- "requires": {
- "extend-shallow": "2.0.1",
- "is-extendable": "0.1.1",
- "is-plain-object": "2.0.4",
- "to-object-path": "0.3.0"
- }
- }
- }
- },
- "uniq": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
- "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
- "dev": true
- },
- "uniqs": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz",
- "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=",
- "dev": true
- },
- "unique-filename": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
- "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
- "dev": true,
- "requires": {
- "unique-slug": "2.0.1"
- }
- },
- "unique-slug": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz",
- "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==",
- "dev": true,
- "requires": {
- "imurmurhash": "0.1.4"
- }
- },
- "unquote": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
- "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
- "dev": true
- },
- "unset-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
- "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
- "dev": true,
- "requires": {
- "has-value": "0.3.1",
- "isobject": "3.0.1"
- },
- "dependencies": {
- "has-value": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
- "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
- "dev": true,
- "requires": {
- "get-value": "2.0.6",
- "has-values": "0.1.4",
- "isobject": "2.1.0"
- },
- "dependencies": {
- "isobject": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
- "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
- "dev": true,
- "requires": {
- "isarray": "1.0.0"
- }
- }
- }
- },
- "has-values": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
- "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
- "dev": true
- }
- }
- },
- "upath": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
- "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
- "dev": true
- },
- "uri-js": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
- "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
- "dev": true,
- "requires": {
- "punycode": "2.1.1"
- }
- },
- "urix": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
- "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
- "dev": true
- },
- "url": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
- "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
- "dev": true,
- "requires": {
- "punycode": "1.3.2",
- "querystring": "0.2.0"
- },
- "dependencies": {
- "punycode": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
- "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
- "dev": true
- }
- }
- },
- "use": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
- "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
- "dev": true
- },
- "util": {
- "version": "0.11.1",
- "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
- "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
- "dev": true,
- "requires": {
- "inherits": "2.0.3"
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
- },
- "util.promisify": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
- "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
- "dev": true,
- "requires": {
- "define-properties": "1.1.3",
- "object.getownpropertydescriptors": "2.0.3"
- }
- },
- "uuid": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
- "dev": true
- },
- "v8-compile-cache": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz",
- "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==",
- "dev": true
- },
- "validate-npm-package-license": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
- "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
- "dev": true,
- "requires": {
- "spdx-correct": "3.1.0",
- "spdx-expression-parse": "3.0.0"
- }
- },
- "vendors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.3.tgz",
- "integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==",
- "dev": true
- },
- "verror": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
- "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
- "dev": true,
- "requires": {
- "assert-plus": "1.0.0",
- "core-util-is": "1.0.2",
- "extsprintf": "1.3.0"
- }
- },
- "vm-browserify": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
- "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
- "dev": true,
- "requires": {
- "indexof": "0.0.1"
- }
- },
- "watchpack": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
- "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==",
- "dev": true,
- "requires": {
- "chokidar": "2.1.6",
- "graceful-fs": "4.1.15",
- "neo-async": "2.6.1"
- }
- },
- "webpack": {
- "version": "4.31.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.31.0.tgz",
- "integrity": "sha512-n6RVO3X0LbbipoE62akME9K/JI7qYrwwufs20VvgNNpqUoH4860KkaxJTbGq5bgkVZF9FqyyTG/0WPLH3PVNJA==",
- "dev": true,
- "requires": {
- "@webassemblyjs/ast": "1.8.5",
- "@webassemblyjs/helper-module-context": "1.8.5",
- "@webassemblyjs/wasm-edit": "1.8.5",
- "@webassemblyjs/wasm-parser": "1.8.5",
- "acorn": "6.1.1",
- "acorn-dynamic-import": "4.0.0",
- "ajv": "6.10.0",
- "ajv-keywords": "3.4.0",
- "chrome-trace-event": "1.0.0",
- "enhanced-resolve": "4.1.0",
- "eslint-scope": "4.0.3",
- "json-parse-better-errors": "1.0.2",
- "loader-runner": "2.4.0",
- "loader-utils": "1.2.3",
- "memory-fs": "0.4.1",
- "micromatch": "3.1.10",
- "mkdirp": "0.5.1",
- "neo-async": "2.6.1",
- "node-libs-browser": "2.2.0",
- "schema-utils": "1.0.0",
- "tapable": "1.1.3",
- "terser-webpack-plugin": "1.2.4",
- "watchpack": "1.6.0",
- "webpack-sources": "1.3.0"
- }
- },
- "webpack-cli": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.2.tgz",
- "integrity": "sha512-FLkobnaJJ+03j5eplxlI0TUxhGCOdfewspIGuvDVtpOlrAuKMFC57K42Ukxqs1tn8947/PM6tP95gQc0DCzRYA==",
- "dev": true,
- "requires": {
- "chalk": "2.4.2",
- "cross-spawn": "6.0.5",
- "enhanced-resolve": "4.1.0",
- "findup-sync": "2.0.0",
- "global-modules": "1.0.0",
- "import-local": "2.0.0",
- "interpret": "1.2.0",
- "loader-utils": "1.2.3",
- "supports-color": "5.5.0",
- "v8-compile-cache": "2.0.3",
- "yargs": "12.0.5"
- },
- "dependencies": {
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "3.0.0"
- }
- }
- }
- },
- "webpack-sources": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
- "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
- "dev": true,
- "requires": {
- "source-list-map": "2.0.1",
- "source-map": "0.6.1"
- }
- },
- "which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
- "requires": {
- "isexe": "2.0.0"
- }
- },
- "which-module": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
- "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
- "dev": true
- },
- "wide-align": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
- "dev": true,
- "requires": {
- "string-width": "2.1.1"
- }
- },
- "worker-farm": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
- "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
- "dev": true,
- "requires": {
- "errno": "0.1.7"
- }
- },
- "wrap-ansi": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
- "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
- "dev": true,
- "requires": {
- "string-width": "1.0.2",
- "strip-ansi": "3.0.1"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
- "requires": {
- "number-is-nan": "1.0.1"
- }
- },
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
- "requires": {
- "code-point-at": "1.1.0",
- "is-fullwidth-code-point": "1.0.0",
- "strip-ansi": "3.0.1"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
- "requires": {
- "ansi-regex": "2.1.1"
- }
- }
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
- },
- "xtend": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
- "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
- "dev": true
- },
- "y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
- "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
- "dev": true
- },
- "yallist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
- "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
- "dev": true
- },
- "yargs": {
- "version": "12.0.5",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
- "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
- "dev": true,
- "requires": {
- "cliui": "4.1.0",
- "decamelize": "1.2.0",
- "find-up": "3.0.0",
- "get-caller-file": "1.0.3",
- "os-locale": "3.1.0",
- "require-directory": "2.1.1",
- "require-main-filename": "1.0.1",
- "set-blocking": "2.0.0",
- "string-width": "2.1.1",
- "which-module": "2.0.0",
- "y18n": "4.0.0",
- "yargs-parser": "11.1.1"
- }
- },
- "yargs-parser": {
- "version": "11.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
- "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
- "dev": true,
- "requires": {
- "camelcase": "5.3.1",
- "decamelize": "1.2.0"
- }
- }
- }
-}
diff --git a/sass/package.json b/sass/package.json
deleted file mode 100644
index b07b64d..0000000
--- a/sass/package.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "name": "travelynx",
- "version": "1.0.0",
- "description": "travelynx - Railway Travel Logger",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/derf/travelynx.git"
- },
- "author": "",
- "license": "MIT",
- "bugs": {
- "url": "https://github.com/derf/travelynx/issues"
- },
- "homepage": "https://github.com/derf/travelynx#readme",
- "devDependencies": {
- "css-loader": "^2.1.1",
- "cssnano": "^4.1.10",
- "materialize-css": "^1.0.0-rc.2",
- "mini-css-extract-plugin": "^0.6.0",
- "node-sass": "^4.12.0",
- "postcss": "^7.0.16",
- "postcss-loader": "^3.0.0",
- "postcss-preset-env": "^6.6.0",
- "postcss-scss": "^2.0.0",
- "sass-loader": "^7.1.0",
- "webpack": "^4.31.0",
- "webpack-cli": "^3.3.2"
- }
-}
diff --git a/sass/src/common/index.scss b/sass/src/common/index.scss
index f279240..b2d3187 100644
--- a/sass/src/common/index.scss
+++ b/sass/src/common/index.scss
@@ -22,6 +22,37 @@ a.unmarked {
color: $off-black;
}
+.white-text a {
+ color: #eeeeff;
+}
+
+div.targetlist {
+ display: grid;
+ grid-template-columns: 1fr max-content;
+ align-items: center;
+ > 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);
+ .material-icons {
+ vertical-align: bottom;
+ margin-bottom: 0.2em;
+ }
+ span {
+ display: inline-block;
+ }
+}
+
.pagination {
li {
a {
@@ -29,3 +60,13 @@ a.unmarked {
}
}
}
+
+.collection-item {
+ color: $off-black;
+ .secondary-content {
+ color: $off-black;
+ }
+ &.disabled {
+ color: $inactive-color;
+ }
+}
diff --git a/sass/src/common/local.scss b/sass/src/common/local.scss
new file mode 100644
index 0000000..c3fe29c
--- /dev/null
+++ b/sass/src/common/local.scss
@@ -0,0 +1,297 @@
+.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;
+ }
+}
+
+// departure board and history - focus highlight
+.collection.departures > li,
+.collection.history > li {
+ transition: background .3s;
+ display: grid;
+ &:not(#now,.history-date-change ):hover, &:focus-within {
+ background-color: $departures-highlight-color;
+ outline: 2px solid $link-color;
+ }
+}
+
+// departure board - layout
+
+.collection.departures li {
+ grid-template-columns: 10ch 10ch 1fr;
+ align-items: center;
+ &#now {
+ background-color: $departures-highlight-color;
+ padding: 2rem 20px;
+ grid-template-columns: 10ch 1fr;
+ strong {
+ font-weight: bold;
+ }
+ }
+ &.cancelled {
+ background-color: $departures-cancelled-color;
+ font-style: italic;
+ .dep-line {
+ background-color: transparent;
+ border: 1px solid;
+ color: $off-black;
+ }
+ .dep-time::after {
+ content: " ⊖";
+ font-style: normal;
+ }
+ }
+}
+.departures .dep-time {
+ color: $off-black;
+ &:focus {
+ outline: none;
+ }
+}
+.departures .dep-dest {
+ margin-left: 0.8rem;
+ .followee-checkin {
+ font-size: 0.9rem;
+ display: block;
+ i.material-icons {
+ vertical-align: middle;
+ }
+ }
+}
+
+// history - layout
+
+.collection.history > li {
+ display: grid;
+ grid-template-columns: 10ch 1fr;
+ grid-template-rows: 1fr;
+ a:first-child {
+ align-self: center;
+ text-align: center;
+ display: flex;
+ }
+ &.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;
+ a {
+ font-family: $font-stack;
+ }
+ strong {
+ font-weight: 600;
+ }
+
+ // route icon bubble
+ i.material-icons {
+ &[aria-label=nach] {
+ padding-top: 0.4rem;
+ }
+ &[aria-label=von] {
+ display: block;
+ transform: rotate(-90deg);
+ height: 1rem;
+ margin-top: 0.4rem;
+ }
+ }
+
+ // route line / "perlenschnur"
+ &::before {
+ content: '';
+ background: $off-black;
+ position: absolute;
+ width: 2px;
+ left: calc( (1rem - 2px) / 2 );
+ bottom: 0;
+ top: 0;
+ }
+ &:first-of-type::before {
+ top: 1.3rem;
+ }
+ &:last-of-type::before {
+ bottom: unset;
+ height: 0.5rem;
+ }
+}
+
+// train color bubbles
+.dep-line {
+ text-align: center;
+ padding: .2rem;
+ color: white;
+ background: color('grey', 'darken-3');
+ border-radius: .2rem;
+ display: inline-block;
+ font-weight: 600;
+ line-height: 1;
+ height: fit-content;
+ width: fit-content;
+ min-width: 6ch;
+ margin: 0 auto;
+
+ &.Bus, &.RUF, &.AST {
+ background-color: #a3167e;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.STR {
+ background-color: #c5161c;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.S, &.RS, &.RER, &.SKW {
+ background-color: #008d4f;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.U, &.STB, &.M {
+ background-color: #014e8d;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.RE, &.IRE, &.REX {
+ background-color: #ff4f00;
+ }
+ &.RB, &.MEX, &.TER, &.R {
+ background-color: #1f4a87;
+ }
+ // DE
+ &.IC, &.ICE, &.EC, &.ECE, &.D,
+ // CH
+ &.IR,
+ // FR
+ &.TGV, &.OGV, &.EST,
+ // PL
+ &.TLK, &.EIC {
+ background-color: #ff0404;
+ font-weight: 900;
+ font-style: italic;
+ padding: .2rem;
+ }
+ &.RJ, &.RJX {
+ background-color: #c63131;
+ }
+ &.NJ, &.EN {
+ background-color: #29255b;
+ }
+ &.WB {
+ background-color: #2e85ce;
+ }
+ &.FLX {
+ background-color: #71d800;
+ color: black;
+ }
+}
+
+.departures.connections {
+ li {
+ grid-template-columns: 15ch 10ch 1fr;
+ }
+ .connect-platform-wrapper {
+ text-align: center;
+ span {
+ display: block;
+ }
+ }
+}
+
+
+@media screen and (max-width: 600px) {
+ .collection.departures li {
+ grid-template-columns: 10ch 1fr;
+ .dep-line, .dep-time, .connect-platform-wrapper {
+ grid-column: 1;
+ text-align: center;
+ }
+ .dep-dest {
+ grid-column: 2;
+ grid-row: 1 / span 2;
+ }
+ }
+ .departures.connections li {
+ grid-template-columns: 15ch 1fr;
+ .connect-platform-wrapper span {
+ display: inline-block;
+ }
+ }
+}
diff --git a/sass/src/dark/_variables.scss b/sass/src/dark/_variables.scss
index caa282e..6665269 100644
--- a/sass/src/dark/_variables.scss
+++ b/sass/src/dark/_variables.scss
@@ -1,18 +1,26 @@
$bg-color: #101010 !default;
+$collection-bg-color: $bg-color;
$info-color: color('blue-grey', 'darken-4');
+$inactive-color: color('grey', 'darken-1');
+
$off-black: color('shades', 'white');
-$primary-color: color('materialize-red', 'darken-2');
+$primary-color: color('materialize-red', 'darken-4');
$secondary-color: color('cyan', 'darken-2');
$link-color: color('light-blue', 'darken-1');
-$collection-link-color: color('cyan', 'darken-4');
$success-color: color('green', 'darken-2');
$error-color: color('red', 'darken-2');
-$input-border-color: $off-black;
+$input-label-color: $off-black;
+$collection-border-color: color('grey', 'darken-3');
+$collection-link-color: color('shades', 'white');
+$collection-hover-bg-color: color('grey', 'darken-4');
$radio-empty-color: $off-black !default;
$table-striped-color: color('grey', 'darken-4');
$button-flat-color: $off-black;
$card-bg-color: color('grey', 'darken-4');
$card-link-color: $link-color;
+
+$departures-highlight-color: $table-striped-color;
+$departures-cancelled-color: #702020;
diff --git a/sass/src/dark/index.scss b/sass/src/dark/index.scss
index 3cb280f..28c615b 100644
--- a/sass/src/dark/index.scss
+++ b/sass/src/dark/index.scss
@@ -1,7 +1,8 @@
-@import '../../node_modules/materialize-css/sass/components/color-variables';
+@import '../../components/color-variables';
@import 'variables.scss';
-@import '../../node_modules/materialize-css/sass/materialize.scss';
+@import '../../materialize.scss';
@import '../common/index.scss';
+@import '../common/local.scss';
.progress {
background-color: color('grey', 'darken-3');
diff --git a/sass/src/light/_variables.scss b/sass/src/light/_variables.scss
index abd4f6a..4a634e9 100644
--- a/sass/src/light/_variables.scss
+++ b/sass/src/light/_variables.scss
@@ -2,4 +2,9 @@ $bg-color: #fff;
$info-color: color('yellow', 'lighten-4');
$link-color: color('light-blue', 'darken-1');
$card-link-color: $link-color;
+$collection-link-color: color('shades', 'black');
$card-bg-color: color('blue-grey', 'lighten-5');
+$inactive-color: color('grey', 'darken-2');
+$input-label-color: color('shades', 'black');
+$departures-highlight-color: color('grey', 'lighten-3');
+$departures-cancelled-color: color('red', 'lighten-4');
diff --git a/sass/src/light/index.scss b/sass/src/light/index.scss
index 6360df0..531c346 100644
--- a/sass/src/light/index.scss
+++ b/sass/src/light/index.scss
@@ -1,7 +1,8 @@
-@import '../../node_modules/materialize-css/sass/components/color-variables';
+@import '../../components/color-variables';
@import 'variables.scss';
-@import '../../node_modules/materialize-css/sass/materialize.scss';
+@import '../../materialize.scss';
@import '../common/index.scss';
+@import '../common/local.scss';
.progress {
background-color: color('grey', 'lighten-2');
diff --git a/sass/webpack.config.js b/sass/webpack.config.js
deleted file mode 100644
index 9696dbb..0000000
--- a/sass/webpack.config.js
+++ /dev/null
@@ -1,47 +0,0 @@
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const path = require('path');
-console.log(path.resolve('../public/static/css/'));
-
-module.exports = {
- mode: 'production',
- entry: {
- light: './src/light/index.scss',
- dark: './src/dark/index.scss',
- },
- output: {
- path: path.resolve('../public/static/css/'),
- },
- plugins: [
- new MiniCssExtractPlugin({
- filename: '[name].min.css',
- chunkFilename: '[id].css',
- }),
- ],
- module: {
- rules: [
- {
- test: /\.s?css$/,
- use: [
- MiniCssExtractPlugin.loader,
- {
- loader: 'css-loader',
- options: {
- importLoaders: 2,
- },
- },
- {
- loader: 'postcss-loader',
- options: {
- ident: 'postcss',
- plugins: loader => [
- require('postcss-preset-env')(),
- require('cssnano')(),
- ],
- },
- },
- 'sass-loader',
- ],
- },
- ],
- },
-};
diff --git a/scripts/asset-rebuild b/scripts/asset-rebuild
index dd32113..eb6a140 100755
--- a/scripts/asset-rebuild
+++ b/scripts/asset-rebuild
@@ -1,7 +1,15 @@
#!/bin/sh
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
set -x
-for file in autocomplete geolocation travelynx-actions; do
+for theme in dark light; do
+ sassc -t compressed sass/src/${theme}/index.scss public/static/css/${theme}.min.css
+done
+
+for file in geolocation travelynx-actions; do
uglifyjs public/static/js/${file}.js -c -m > public/static/js/${file}.min.js
done
diff --git a/scripts/asset-release b/scripts/asset-release
index dc4a8a7..1f96d33 100755
--- a/scripts/asset-release
+++ b/scripts/asset-release
@@ -1,5 +1,9 @@
#!/bin/sh
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
set -ex
current="$(find public/static/v* | tail -n 1 | grep -o '..$')"
diff --git a/share/ice_names.json b/share/ice_names.json
new file mode 100755
index 0000000..be386b8
--- /dev/null
+++ b/share/ice_names.json
@@ -0,0 +1,254 @@
+{
+"101": "Gießen",
+"102": "Jever",
+"103": "Neu-Isenburg",
+"104": "Fulda",
+"105": "Offenbach am Main",
+"106": "Itzehoe",
+"107": "Plattling",
+"108": "Lichtenfels",
+"110": "Gelsenkirchen",
+"112": "Memmingen",
+"113": "Frankenthal/Pfalz",
+"114": "Friedrichshafen",
+"115": "Regensburg",
+"116": "Pforzheim",
+"117": "Hof",
+"118": "Gelnhausen",
+"119": "Osnabrück",
+"120": "Lüneburg",
+"152": "Hanau",
+"153": "Neumünster",
+"154": "Flensburg",
+"155": "Rosenheim",
+"156": "Heppenheim/Bergstraße",
+"157": "Landshut",
+"158": "Gütersloh",
+"159": "Bad Oldesloe",
+"160": "Mülheim an der Ruhr",
+"161": "Bebra",
+"162": "Geisenheim/Rheingau",
+"166": "Gelnhausen",
+"167": "Garmisch-Partenkirchen",
+"168": "Crailsheim",
+"169": "Worms",
+"171": "Heusenstamm",
+"172": "Aschaffenburg",
+"173": "Basel",
+"174": "Zürich",
+"175": "Nürnberg",
+"176": "Bremen",
+"177": "Rendsburg",
+"178": "Bremerhaven",
+"180": "Castrop-Rauxel",
+"181": "Interlaken",
+"182": "Rüdesheim am Rhein",
+"183": "Timmendorfer Strand",
+"184": "Bruchsal",
+"185": "Freilassing",
+"186": "Chur",
+"187": "Mühldorf a. Inn",
+"188": "Hildesheim",
+"190": "Ludwigshafen am Rhein",
+"201": "Rheinsberg",
+"202": "Wuppertal",
+"203": "Cottbus/Chóśebuz",
+"204": "Bielefeld",
+"205": "Zwickau",
+"206": "Magdeburg",
+"207": "Stendal",
+"208": "Bonn",
+"209": "Riesa",
+"210": "Fontanestadt Neuruppin",
+"211": "Uelzen",
+"212": "Potsdam",
+"213": "Nauen",
+"214": "Hamm (Westf.)",
+"215": "Bitterfeld-Wolfen",
+"216": "Dessau",
+"217": "Bergen auf Rügen",
+"218": "Braunschweig",
+"219": "Hagen",
+"220": "Meiningen",
+"221": "Lübbenau/Spreewald",
+"222": "Eberswalde",
+"223": "Schwerin",
+"224": "Saalfeld (Saale)",
+"225": "Oldenburg (Oldb)",
+"226": "Lutherstadt Wittenberg",
+"227": "Ludwigslust",
+"228": "Altenburg",
+"229": "Templin",
+"230": "Delitzsch",
+"231": "Brandenburg an der Havel",
+"232": "Frankfurt (Oder)",
+"233": "Ulm",
+"234": "Minden",
+"235": "Görlitz",
+"236": "Jüterbog",
+"237": "Neustrelitz",
+"238": "Saarbrücken",
+"239": "Essen",
+"240": "Bochum",
+"241": "Bad Hersfeld",
+"242": "Quedlinburg",
+"243": "Bautzen/Budyšin",
+"244": "Koblenz",
+"301": "Freiburg im Breisgau",
+"302": "Hansestadt Lübeck",
+"303": "Dortmund",
+"304": "München",
+"305": "Baden-Baden",
+"306": "Nördlingen",
+"307": "Oberhausen",
+"308": "Murnau am Staffelsee",
+"309": "Aalen",
+"310": "Wolfsburg",
+"311": "Wiesbaden",
+"312": "Montabaur",
+"313": "Treuchtlingen",
+"314": "Bergisch Gladbach",
+"315": "Singen (Hohentwiel)",
+"316": "Siegburg",
+"317": "Recklinghausen",
+"318": "Münster (Westf.)",
+"319": "Duisburg",
+"320": "Weil am Rhein",
+"321": "Krefeld",
+"322": "Solingen",
+"323": "Schaffhausen",
+"324": "Fürth",
+"325": "Ravensburg",
+"326": "Neunkirchen",
+"327": "Siegen",
+"328": "Aachen",
+"330": "Göttingen",
+"331": "Westerland/Sylt",
+"332": "Augsburg",
+"333": "Goslar",
+"334": "Offenburg",
+"335": "Konstanz",
+"336": "Ingolstadt",
+"337": "Stuttgart",
+"351": "Herford",
+"352": "Mönchengladbach",
+"353": "Neu-Ulm",
+"354": "Mittenwald",
+"355": "Tuttlingen",
+"357": "Esslingen am Neckar",
+"358": "St. Ingbert",
+"359": "Leverkusen",
+"360": "Linz am Rhein",
+"361": "Celle",
+"362": "Schwerte (Ruhr)",
+"363": "Weilheim i. OB",
+"1101": "Neustadt an der Weinstraße",
+"1102": "Neubrandenburg",
+"1103": "Paderborn",
+"1104": "Erfurt",
+"1105": "Dresden",
+"1107": "Pirna",
+"1108": "Berlin",
+"1109": "Güstrow",
+"1110": "Naumburg (Saale)",
+"1111": "Hansestadt Wismar",
+"1112": "Freie und Hansestadt Hamburg",
+"1113": "Hansestadt Stralsund",
+"1117": "Erlangen",
+"1118": "Plauen/Vogtland",
+"1119": "Meißen",
+"1125": "Arnstadt",
+"1126": "Leipzig",
+"1127": "Weimar",
+"1128": "Reutlingen",
+"1129": "Kiel",
+"1130": "Jena",
+"1131": "Trier",
+"1132": "Wittenberge",
+"1151": "Elsterwerda",
+"1152": "Travemünde",
+"1153": "Ilmenau",
+"1154": "Sonneberg",
+"1155": "Mühlhausen/Thüringen",
+"1156": "Waren (Müritz)",
+"1157": "Innsbruck",
+"1158": "Falkenberg/Elster",
+"1159": "Passau",
+"1160": "Markt Holzkirchen",
+"1161": "Andernach",
+"1162": "Vaihingen an der Enz",
+"1163": "Ostseebad Binz",
+"1164": "Rödental",
+"1165": "Bad Oeynhausen",
+"1166": "Bingen am Rhein",
+"1167": "Traunstein",
+"1168": "Ellwangen",
+"1169": "Tutzing",
+"1170": "Prenzlau",
+"1171": "Oschatz",
+"1172": "Bamberg",
+"1173": "Halle (Saale)",
+"1174": "Hansestadt Warburg",
+"1175": "Villingen-Schwenningen",
+"1176": "Coburg",
+"1177": "Rathenow",
+"1178": "Ostseebad Warnemünde",
+"1180": "Darmstadt",
+"1181": "Horb am Neckar",
+"1182": "Mainz",
+"1183": "Oberursel (Taunus)",
+"1184": "Kaiserslautern",
+"1190": "Wien",
+"1191": "Salzburg",
+"1192": "Linz",
+"1501": "Eisenach",
+"1502": "Karlsruhe",
+"1503": "Altenbeken",
+"1504": "Heidelberg",
+"1505": "Marburg/Lahn",
+"1506": "Kassel",
+"1520": "Gotha",
+"1521": "Homburg/Saar",
+"1522": "Torgau",
+"1523": "Hansestadt Greifswald",
+"1524": "Hansestadt Rostock",
+"2853": "Nationalpark Sächsische Schweiz",
+"2865": "Remstal",
+"2868": "Nationalpark Niedersächsisches Wattenmeer",
+"2871": "Leipziger Neuseenland",
+"2874": "Oberer Neckar",
+"2875": "Magdeburger Börde",
+"4103": "Allgäu",
+"4111": "Gäu",
+"4114": "Dresden Elbland",
+"4117": "Mecklenburgische Ostseeküste",
+"4601": "Europa/Europe",
+"4602": "Euregio Maas-Rhein",
+"4603": "Mannheim",
+"4604": "Brussel/Bruxelles",
+"4607": "Hannover",
+"4610": "Frankfurt am Main",
+"4611": "Düsseldorf",
+"4651": "Amsterdam",
+"4652": "Arnhem",
+"4680": "Würzburg",
+"4682": "Köln",
+"4683": "Limburg an der Lahn",
+"4684": "Forbach-Lorraine",
+"4685": "Schwäbisch Hall",
+"4712": "Dillingen a.d. Donau",
+"4710": "Ansbach",
+"4717": "Paris",
+"8007": "Rheinland",
+"9006": "Martin Luther",
+"9018": "Freistaat Bayern",
+"9025": "Nordrhein-Westfalen",
+"9026": "Zürichsee",
+"9028": "Freistaat Sachsen",
+"9041": "Baden-Württemberg",
+"9046": "Female ICE",
+"9050": "Metropole Ruhr",
+"9202": "Schleswig-Holstein",
+"9457": "Bundesrepublik Deutschland",
+"9481": "Rheinland-Pfalz"
+}
diff --git a/share/old_station_names.json b/share/old_station_names.json
new file mode 100755
index 0000000..60a0d6f
--- /dev/null
+++ b/share/old_station_names.json
@@ -0,0 +1,136 @@
+{
+ "Amersfoort" : "Amersfoort Centraal",
+ "Barcelona Sants" : "Barcelona Sant Andreu Comtal",
+ "Barchel, Oerel" : "Barchel",
+ "Berlin Betriebsbf Schöneweide" : "Berlin-Johannisthal",
+ "Berlin Gehrenseestr." : "Berlin Gehrenseestraße",
+ "Berlin Hbf (tief)" : "Berlin Hbf",
+ "Berlin Wannsee" : "Berlin-Wannsee",
+ "Berlin Wannsee (S)" : "Berlin-Wannsee (S)",
+ "Berlin-Schönefeld Flughafen" : "Flughafen BER - Terminal 5 (Schönefeld)",
+ "Bernburg" : "Bernburg Hbf",
+ "Biedenkopf-Schulzentrum" : "Biedenkopf Campus",
+ "Binsfeld" : "Nörvenich-Binsfeld",
+ "Blainville-Damelevieres" : "Blainville-Damelevières",
+ "Blumenkamp" : "Wesel-Blumenkamp",
+ "Bockum-Hövel" : "Hamm-Bockum-Hövel",
+ "Brackwede" : "Bielefeld-Brackwede",
+ "Bruchsal Tunnelstr" : "Bruchsal Tunnelstraße",
+ "Brügge(Westf)" : "Lüdenscheid-Brügge",
+ "Bösensell" : "Senden-Bösensell",
+ "Chambery-Challes-E" : "Chambéry-Challes-les-Eaux",
+ "Charleroi Sud" : "Charleroi Central",
+ "Cottbus" : "Cottbus Hbf",
+ "Delft Zuid" : "Delft Campus",
+ "Dieringhausen" : "Gummersbach-Dieringhausen",
+ "Dingden" : "Hamminkeln-Dingden",
+ "Einbeck Salzderhelden" : "Einbeck-Salzderhelden",
+ "Eindhoven" : "Eindhoven Centraal",
+ "Eisenbach-Matzenbach" : "Matzenbach",
+ "Ergste" : "Schwerte-Ergste",
+ "Escherndorf-Vogelburg" : "Escherndorf-Vogelsburg",
+ "Essel, Kutenholz" : "Essel",
+ "Europapl./PostGalerie (Kaiserstr), Karlsruhe" : "Europaplatz/Postgal. (Kaiser), Karlsruhe",
+ "Europapl./PostGalerie (Karlstr.), Karlsruhe" : "Europaplatz/Postgalerie (Karlstr.), Karlsruhe",
+ "Europapl./Postgalerie (Karl), Karlsruhe" : "Europaplatz/Postgalerie (Karls, Karlsruhe",
+ "Europaplatz/Postgal. (Kaiser), Karlsruhe" : "Europaplatz/Postgalerie, Karlsruhe",
+ "Europaplatz/Postgalerie (Karls, Karlsruhe" : "Europaplatz/Postgalerie (Karlstr.), Karlsruhe",
+ "Flughafen BER - Terminal 1-2" : "Flughafen BER",
+ "Flughafen BER - Terminal 1-2 (S-Bahn)" : "Flughafen BER (S-Bahn)",
+ "Flughafen BER - Terminal 5 (Schönefeld)" : "Schönefeld(bei Berlin)",
+ "Freiburg West" : "Freiburg-Landwasser",
+ "Frömern" : "Fröndenberg-Frömern",
+ "Furth i Wald" : "Furth im Wald",
+ "Fürth-Unterfarrnbach" : "Fürth-Klinikum",
+ "Galgenschanze" : "Kaiserslautern Galgenschanze",
+ "Germersheim Bahnhof" : "Germersheim",
+ "Glossen (b Oschatz)" : "Glossen(b Oschatz)",
+ "Godelheim" : "Höxter-Godelheim",
+ "Gondelsheim Schloßstadion" : "Gondelsheim Schlossstadion",
+ "Hagen, Stade" : "Hagen(Kr. Stade)",
+ "Halle(W) Gerry-Weber-Stadion" : "Halle(Westf) OWL-Arena",
+ "Hamm(Westf)" : "Hamm(Westf)Hbf",
+ "Hattingen(R) Mitte" : "Hattingen(Ruhr) Mitte",
+ "Heessen" : "Hamm-Heessen",
+ "Holzgerlingen Nord" : "Holzgerlingen Hülben",
+ "Holzhausen-Heddinghausen" : "Bad Holzhausen",
+ "Hummelberg" : "Berghausen Hummelberg",
+ "Jakobwüllesheim" : "Vettweiß-Jakobwüllesheim",
+ "Karlsruhe Albtalbf" : "Karlsruhe Albtalbahnhof",
+ "Karlsruhe Durlacher Tor" : "Karlsruhe Durlacher Tor / KIT-Campus Süd",
+ "Karlsruhe Durlacher Tor / KIT-Campus Süd" : "Karlsruhe Durlacher Tor/KIT-Campus Süd",
+ "Karlsruhe Mühlburger Tor" : "Karlsruhe Mühlburger Tor (Kaiserallee)",
+ "Karlsruhe Mühlburger Tor (Kaiserallee)" : "Karlsruhe Mühlburger Tor",
+ "Kavelstorf(Kr Rostock)" : "Kavelstorf",
+ "Kelenföld" : "Budapest-Kelenföld",
+ "Korbach" : "Korbach Hbf",
+ "Kronenplatz (Fritz-Erler-Str.), Karlsruhe" : "Kronenplatz, Karlsruhe",
+ "Kronenplatz (Kaiserstraße), Karlsruhe" : "Kronenplatz (U), Karlsruhe",
+ "Krumbach(Schw)Schule" : "Krumbach(Schwab)Schule",
+ "Königswinter, Clem.-August-Str." : "Königswinter, Clemens-August-Str.",
+ "Les-Aubrais-Orleans" : "Les Aubrais - Orléans",
+ "Leverkusen-Schlebusch" : "Leverkusen-Manfort",
+ "Lindau Hbf" : "Lindau-Insel",
+ "Lorraine" : "Lorraine TGV",
+ "Lüchtringen" : "Höxter-Lüchtringen",
+ "Malpensa Aeroporto" : "Malpensa Aeroporto T1",
+ "Maria Veen" : "Reken-Maria Veen",
+ "Merseburg" : "Merseburg Hbf",
+ "Mook Molenhoek" : "Mook-Molenhoek",
+ "Mühlburger Tor (Kaiserallee), Karlsruhe" : "Mühlburger Tor, Karlsruhe",
+ "Münster(b Dieburg)" : "Münster(Hessen)",
+ "Nancy Ville" : "Nancy",
+ "Neu Isenburg" : "Neu-Isenburg",
+ "Neubeckum" : "Beckum-Neubeckum",
+ "Neuenbürg(Enz) Eyachbrücke" : "Neuenbürg(Enz)-Rotenbach Eyachbrücke",
+ "Neumarkt-Köstendorf" : "Neumarkt am Wallersee",
+ "Neumarkt/Wallersee" : "Neumarkt am Wallersee",
+ "Niebüll, Sylt Shuttle" : "Niebüll Autoverladung",
+ "Nordbögge" : "Bönen-Nordbögge",
+ "OberurselWeißkirchen/Steinbach" : "Oberursel-Weißkirchen/Steinbach",
+ "Oldenburg(Oldb)" : "Oldenburg(Oldb)Hbf",
+ "Olen" : "Olen(Belgien)",
+ "Ottbergen" : "Höxter-Ottbergen",
+ "Paternion-Feistritz" : "Paternion-Feistritz Bahnhst",
+ "Pfeddersheim" : "Worms-Pfeddersheim",
+ "Preußen" : "Lünen-Preußen",
+ "Prien a Chiemsee" : "Prien am Chiemsee",
+ "Przemysl Gl." : "Przemysl Glowny",
+ "Przylep" : "Zielona Gora Przylep",
+ "Rahden(Kr Lübbecke)" : "Rahden",
+ "Riegel-Malterd.NE" : "Riegel-Malterdingen NE",
+ "Riegel-Malterdingen NE" : "Riegel-Malterdingen (SWEG)",
+ "Ringsheim" : "Ringsheim/Europa-Park",
+ "Rudersdorf(Siegen)" : "Wilnsdorf-Rudersdorf",
+ "Sandebeck" : "Steinheim-Sandebeck",
+ "Sarnau" : "Lahntal-Sarnau",
+ "Schönow(Angerm)" : "Schönow(Uckermark)",
+ "Sennelager" : "Paderborn-Sennelager",
+ "Sennestadt" : "Bielefeld-Sennestadt",
+ "Siegen" : "Siegen Hbf",
+ "St Augustin Markt" : "Sankt Augustin Zentrum",
+ "St-Gervais-les-Bains" : "St-Gervais-les-Bains-le-Fayet",
+ "St. Margrethen" : "St. Margrethen SG",
+ "Stendal" : "Stendal Hbf",
+ "Stockach NE" : "Stockach",
+ "Stryck" : "Willingen-Stryck",
+ "Sylbach" : "Bad Salzuflen-Sylbach",
+ "Teisnach Rohde&Schwarz" : "Teisnach Rohde+Schwarz",
+ "Thalheim (b Oschatz)" : "Thalheim(b Oschatz)",
+ "Timmendorferstrand" : "Timmendorfer Strand",
+ "Untersulzbach" : "Sulzbachtal",
+ "Urmitz" : "Mülheim-Kärlich",
+ "Velesyn mesto" : "Velesin mesto",
+ "Vogelsang(Gransee)" : "Vogelsang(Kr Oberhavel)",
+ "Volkswohnung/Staatstheater, Karlsruhe" : "Kongresszentrum, Karlsruhe",
+ "Waggonfabrik" : "Mainz Waggonfabrik",
+ "Waldkraiburg" : "Waldkraiburg-Kraiburg",
+ "Wehrden" : "Beverungen-Wehrden",
+ "Weinheim(Bergstr)" : "Weinheim(Bergstr)Hbf",
+ "Werningerode" : "Weringerode Hbf",
+ "Westerland(Sylt), Sylt Shuttle" : "Westerland (Sylt) Autoverladung",
+ "Wilhelmshaven Hbf" : "Wilhelmshaven",
+ "Windelsbleiche" : "Bielefeld-Windelsbleiche",
+ "Wodzislaw Sl." : "Wodzislaw Slaski",
+ "Wusterhausen(Dosse) NE" : "Wusterhausen(Dosse)"
+}
diff --git a/share/old_stations.json b/share/old_stations.json
new file mode 100644
index 0000000..b3831e7
--- /dev/null
+++ b/share/old_stations.json
@@ -0,0 +1,2603 @@
+[
+ {
+ "ds100" : "AARF",
+ "eva" : 8071338,
+ "latlong" : [
+ 53.233409,
+ 8.818139
+ ],
+ "name" : "Ahrensfelde(Bz. Bremen)"
+ },
+ {
+ "ds100" : "XIAE",
+ "eva" : 8300358,
+ "latlong" : [
+ 43.8707,
+ 7.553207
+ ],
+ "name" : "Airole"
+ },
+ {
+ "ds100" : "XIAO",
+ "eva" : 8300133,
+ "latlong" : [
+ 44.007929,
+ 8.171387
+ ],
+ "name" : "Alassio"
+ },
+ {
+ "ds100" : "XUAI",
+ "eva" : 5300096,
+ "latlong" : [
+ 46.0563473,
+ 23.576231
+ ],
+ "name" : "Alba Iulia"
+ },
+ {
+ "ds100" : "XIAT",
+ "eva" : 8300950,
+ "latlong" : [
+ 45.783184,
+ 9.079832
+ ],
+ "name" : "Albate-Camerlata"
+ },
+ {
+ "ds100" : "XIAB",
+ "eva" : 8300132,
+ "latlong" : [
+ 44.047308,
+ 8.221556
+ ],
+ "name" : "Albenga"
+ },
+ {
+ "ds100" : "FALH",
+ "eva" : 8070252,
+ "latlong" : [
+ 49.644776,
+ 8.096752
+ ],
+ "name" : "Albisheim(Pfrimm)"
+ },
+ {
+ "ds100" : "HAHS",
+ "eva" : 8000486,
+ "latlong" : [
+ 52.50347141,
+ 7.964338
+ ],
+ "name" : "Alfhausen"
+ },
+ {
+ "ds100" : "MAF",
+ "eva" : 8026354,
+ "latlong" : [
+ 48.554979,
+ 12.102813
+ ],
+ "name" : "Altdorf(Niederbay)"
+ },
+ {
+ "ds100" : "DAMG",
+ "eva" : 8070697,
+ "latlong" : [
+ 51.237787,
+ 13.040356
+ ],
+ "name" : "Altmügeln"
+ },
+ {
+ "ds100" : "TAM M",
+ "eva" : 8079075,
+ "latlong" : [
+ 48.577852,
+ 9.873959
+ ],
+ "name" : "Amstetten(W) Lokalbahn"
+ },
+ {
+ "ds100" : "XUA",
+ "eva" : 5300002,
+ "latlong" : [
+ 46.189565,
+ 21.325546
+ ],
+ "name" : "Arad"
+ },
+ {
+ "ds100" : "XSABS",
+ "eva" : 8506110,
+ "latlong" : [
+ 47.5126421316245,
+ 9.44032100861396
+ ],
+ "name" : "Arbon (See)"
+ },
+ {
+ "ds100" : "XIAZ",
+ "eva" : 8300179,
+ "latlong" : [
+ 43.461078,
+ 11.875385
+ ],
+ "name" : "Arezzo"
+ },
+ {
+ "ds100" : "XKA",
+ "eva" : 7000107,
+ "latlong" : [
+ 51.143431,
+ 0.875153
+ ],
+ "name" : "Ashford(Kent)"
+ },
+ {
+ "ds100" : "XKAI",
+ "eva" : 7098107,
+ "latlong" : [
+ 51.143256,
+ 0.874723
+ ],
+ "name" : "Ashford(Kent) Int."
+ },
+ {
+ "ds100" : "UAS",
+ "eva" : 8011060,
+ "latlong" : [
+ 51.10203,
+ 11.590437
+ ],
+ "name" : "Auerstedt"
+ },
+ {
+ "ds100" : "LDU",
+ "eva" : 8011070,
+ "latlong" : [
+ 51.587623,
+ 12.598815
+ ],
+ "name" : "Bad Düben(Mulde)"
+ },
+ {
+ "ds100" : "MBEP",
+ "eva" : 8000700,
+ "latlong" : [
+ 47.884007,
+ 12.632888
+ ],
+ "name" : "Bad Empfing"
+ },
+ {
+ "ds100" : "TIMN",
+ "eva" : 8070309,
+ "latlong" : [
+ 48.404155,
+ 8.770092
+ ],
+ "name" : "Bad Imnau"
+ },
+ {
+ "ds100" : "LBSS",
+ "eva" : 8011083,
+ "latlong" : [
+ 51.6698138,
+ 12.7258694
+ ],
+ "name" : "Bad Schmiedeberg Süd"
+ },
+ {
+ "ds100" : "UBSN",
+ "eva" : 8011086,
+ "latlong" : [
+ 51.097428,
+ 11.6283
+ ],
+ "name" : "Bad Sulza Nord"
+ },
+ {
+ "ds100" : "TBW",
+ "eva" : 8000769,
+ "latlong" : [
+ 47.910258,
+ 9.889739
+ ],
+ "name" : "Bad Wurzach"
+ },
+ {
+ "ds100" : "ZWBA",
+ "eva" : 2100013,
+ "latlong" : [
+ 53.142611,
+ 26.027078
+ ],
+ "name" : "Baranovichi Centralnye"
+ },
+ {
+ "ds100" : "ABCL",
+ "eva" : 8071336,
+ "latlong" : [
+ 53.471851,
+ 9.027667
+ ],
+ "name" : "Barchel"
+ },
+ {
+ "ds100" : "LBAS",
+ "eva" : 8011112,
+ "latlong" : [
+ 52.214328,
+ 11.640852
+ ],
+ "name" : "Barleber See"
+ },
+ {
+ "ds100" : "XIBT",
+ "eva" : 8300397,
+ "latlong" : [
+ 40.605894,
+ 14.983259
+ ],
+ "name" : "Battipaglia"
+ },
+ {
+ "ds100" : "XSBLS",
+ "eva" : 8506159,
+ "latlong" : [
+ 47.6753379822752,
+ 9.0176698078502
+ ],
+ "name" : "Berlingen URh"
+ },
+ {
+ "ds100" : "XLXBB",
+ "eva" : 8271141,
+ "latlong" : [
+ 49.472074,
+ 6.107859
+ ],
+ "name" : "Bettembourg(fr)"
+ },
+ {
+ "ds100" : "XIBVA",
+ "eva" : 8301199,
+ "latlong" : [
+ 43.825194,
+ 7.579073
+ ],
+ "name" : "Bevera"
+ },
+ {
+ "ds100" : "RBIC",
+ "eva" : 8077777,
+ "latlong" : [
+ 47.967528,
+ 9.101812
+ ],
+ "name" : "Bichtlingen"
+ },
+ {
+ "ds100" : "RBZN",
+ "eva" : 8070323,
+ "latlong" : [
+ 47.631409,
+ 7.621048
+ ],
+ "name" : "Binzen"
+ },
+ {
+ "ds100" : "BBFD",
+ "eva" : 8089186,
+ "latlong" : [
+ 52.3380121,
+ 13.4156182
+ ],
+ "name" : "Blankenfelde (S)"
+ },
+ {
+ "ds100" : "LBLG",
+ "eva" : 8010056,
+ "latlong" : [
+ 52.033806,
+ 11.457093
+ ],
+ "name" : "Blumenberg"
+ },
+ {
+ "ds100" : "XIBD",
+ "eva" : 8300136,
+ "latlong" : [
+ 43.778323,
+ 7.663572
+ ],
+ "name" : "Bordighera"
+ },
+ {
+ "ds100" : "XDBOP",
+ "eva" : 8600276,
+ "latlong" : [
+ 55.494894,
+ 11.972927
+ ],
+ "name" : "Borup st"
+ },
+ {
+ "ds100" : "XFBZV",
+ "eva" : 8700333,
+ "latlong" : [
+ 49.289733,
+ 6.529723
+ ],
+ "name" : "Bouzonville"
+ },
+ {
+ "ds100" : "XTXBE",
+ "eva" : 5403739,
+ "latlong" : [
+ 48.712551,
+ 16.868402
+ ],
+ "name" : "Breclav(Gr)"
+ },
+ {
+ "ds100" : "KBK",
+ "eva" : 8071651,
+ "latlong" : [
+ 50.432442,
+ 7.168888
+ ],
+ "name" : "Brenk"
+ },
+ {
+ "ds100" : "ZWB",
+ "eva" : 2100001,
+ "latlong" : [
+ 52.100432,
+ 23.680681
+ ],
+ "name" : "Brest Central"
+ },
+ {
+ "ds100" : "XPXTE",
+ "eva" : 2100149,
+ "latlong" : [
+ 52.098902,
+ 23.673854
+ ],
+ "name" : "Brest(Gr)"
+ },
+ {
+ "ds100" : "XTBDN",
+ "eva" : 5438015,
+ "latlong" : [
+ 49.182897,
+ 16.615733
+ ],
+ "name" : "Brno dolni nadrazi"
+ },
+ {
+ "ds100" : "XTBRT",
+ "eva" : 5400612,
+ "latlong" : [
+ 50.951596,
+ 14.438788
+ ],
+ "name" : "Brtniky"
+ },
+ {
+ "ds100" : "XABU",
+ "eva" : 8100697,
+ "latlong" : [
+ 46.894811,
+ 15.834249
+ ],
+ "name" : "Burgfried b.Gnas"
+ },
+ {
+ "ds100" : "XEBU",
+ "eva" : 7100005,
+ "latlong" : [
+ 42.371166,
+ -3.666028
+ ],
+ "name" : "Burgos Rosa de Lima"
+ },
+ {
+ "ds100" : "TBUW",
+ "eva" : 8001281,
+ "latlong" : [
+ 47.922765,
+ 9.343805
+ ],
+ "name" : "Burgweiler"
+ },
+ {
+ "ds100" : "NBOB",
+ "eva" : 8070667,
+ "latlong" : [
+ 49.052994,
+ 13.014504
+ ],
+ "name" : "Böbrach"
+ },
+ {
+ "ds100" : "XECM",
+ "eva" : 7100076,
+ "latlong" : [
+ 41.841178,
+ 2.800607
+ ],
+ "name" : "Caldes de Malavella"
+ },
+ {
+ "ds100" : "XICTL",
+ "eva" : 8300375,
+ "latlong" : [
+ 44.496372,
+ 7.591296
+ ],
+ "name" : "Centallo"
+ },
+ {
+ "ds100" : "XTCC",
+ "eva" : 5400730,
+ "latlong" : [
+ 50.454323,
+ 13.363351
+ ],
+ "name" : "Cernovice u Chomutova"
+ },
+ {
+ "ds100" : "XTTR",
+ "eva" : 5400002,
+ "latlong" : [
+ 49.897324,
+ 16.446808
+ ],
+ "name" : "Ceska Trebova"
+ },
+ {
+ "ds100" : "DC B",
+ "eva" : 8071816,
+ "latlong" : [
+ 50.833196,
+ 12.925169
+ ],
+ "name" : "Chemnitz Stefan-Heym-Platz"
+ },
+ {
+ "ds100" : "ONCH",
+ "eva" : 8400152,
+ "latlong" : [
+ 50.876110076904,
+ 6.213844
+ ],
+ "name" : "Chevremont(NL)"
+ },
+ {
+ "ds100" : "XICU",
+ "eva" : 8300148,
+ "latlong" : [
+ 43.002469,
+ 11.957897
+ ],
+ "name" : "Chiusi-Chianciano Terme"
+ },
+ {
+ "ds100" : "ZUC",
+ "eva" : 2200010,
+ "latlong" : [
+ 48.432709,
+ 22.2055488
+ ],
+ "name" : "Chop"
+ },
+ {
+ "ds100" : "DCOL",
+ "eva" : 8011317,
+ "latlong" : [
+ 51.134287,
+ 12.799053
+ ],
+ "name" : "Colditz"
+ },
+ {
+ "ds100" : "XUCU",
+ "eva" : 5300001,
+ "latlong" : [
+ 46.341013,
+ 21.2977
+ ],
+ "name" : "Curtici"
+ },
+ {
+ "ds100" : "XPCD",
+ "eva" : 5100251,
+ "latlong" : [
+ 49.915103,
+ 19.00465
+ ],
+ "name" : "Czechowice-Dziedzice"
+ },
+ {
+ "ds100" : "SDAS",
+ "eva" : 8079081,
+ "latlong" : [
+ 49.148557,
+ 7.777208
+ ],
+ "name" : "Dahn Süd"
+ },
+ {
+ "ds100" : "ADS",
+ "eva" : 8070349,
+ "latlong" : [
+ 53.5319,
+ 9.442568
+ ],
+ "name" : "Deinste"
+ },
+ {
+ "ds100" : "XUD",
+ "eva" : 5300073,
+ "latlong" : [
+ 45.8842727,
+ 22.9102492
+ ],
+ "name" : "Deva"
+ },
+ {
+ "ds100" : "XIDM",
+ "eva" : 8300149,
+ "latlong" : [
+ 43.909659,
+ 8.078514
+ ],
+ "name" : "Diano Marina"
+ },
+ {
+ "ds100" : "XSDHS",
+ "eva" : 8506152,
+ "latlong" : [
+ 47.6904192083997,
+ 8.74885906156379
+ ],
+ "name" : "Diessenhofen URh"
+ },
+ {
+ "ds100" : "XZXDO",
+ "eva" : 7900042,
+ "latlong" : [
+ 45.8906308,
+ 15.6802375
+ ],
+ "name" : "Dobova(Gr)"
+ },
+ {
+ "ds100" : "XTDKR",
+ "eva" : 5400872,
+ "latlong" : [
+ 50.956423,
+ 14.518703
+ ],
+ "name" : "Dolni Krecany"
+ },
+ {
+ "ds100" : "XTDI",
+ "eva" : 5400905,
+ "latlong" : [
+ 50.479439,
+ 13.351523
+ ],
+ "name" : "Domina"
+ },
+ {
+ "ds100" : "TDOD",
+ "eva" : 8029358,
+ "latlong" : [
+ 48.228455,
+ 8.781119
+ ],
+ "name" : "Dotternhausen-Dormettingen"
+ },
+ {
+ "ds100" : "XLDF",
+ "eva" : 8270360,
+ "latlong" : [
+ 50.014874,
+ 6.006621
+ ],
+ "name" : "Drauffelt"
+ },
+ {
+ "ds100" : "EDBI",
+ "eva" : 8001599,
+ "latlong" : [
+ 51.392175,
+ 6.808067
+ ],
+ "name" : "Duisburg-Bissingheim"
+ },
+ {
+ "ds100" : "HDUS",
+ "eva" : 8070358,
+ "latlong" : [
+ 52.924916,
+ 8.627573
+ ],
+ "name" : "Dünsen DHE"
+ },
+ {
+ "ds100" : "NDW",
+ "eva" : 8070805,
+ "latlong" : [
+ 50.357182,
+ 11.516934
+ ],
+ "name" : "Dürrenwaid Bahnhof"
+ },
+ {
+ "ds100" : "XKEI",
+ "eva" : 7004419,
+ "latlong" : [
+ 51.442894,
+ 0.320865
+ ],
+ "name" : "Ebbsfleet International Eurostar"
+ },
+ {
+ "ds100" : "NEHM",
+ "eva" : 8070856,
+ "latlong" : [
+ 49.869412,
+ 10.153426
+ ],
+ "name" : "Eisenheim"
+ },
+ {
+ "ds100" : "TEND",
+ "eva" : 8029356,
+ "latlong" : [
+ 48.257384,
+ 8.83688
+ ],
+ "name" : "Endingen(Württ)"
+ },
+ {
+ "ds100" : "KENG",
+ "eva" : 8070368,
+ "latlong" : [
+ 50.424945,
+ 7.156853
+ ],
+ "name" : "Engeln"
+ },
+ {
+ "ds100" : "XSEMS",
+ "eva" : 8506162,
+ "latlong" : [
+ 47.6752332637346,
+ 9.08544469808097
+ ],
+ "name" : "Ermatingen URh"
+ },
+ {
+ "ds100" : "TERZ",
+ "eva" : 8029357,
+ "latlong" : [
+ 48.255409,
+ 8.815709
+ ],
+ "name" : "Erzingen(Württ)"
+ },
+ {
+ "ds100" : "NESD",
+ "eva" : 8070857,
+ "latlong" : [
+ 49.86674,
+ 10.165791
+ ],
+ "name" : "Escherndorf-Vogelsburg"
+ },
+ {
+ "ds100" : "AESL",
+ "eva" : 8071334,
+ "latlong" : [
+ 53.487691,
+ 9.272382
+ ],
+ "name" : "Essel"
+ },
+ {
+ "ds100" : "LEU",
+ "eva" : 8011522,
+ "latlong" : [
+ 51.827534,
+ 12.638466
+ ],
+ "name" : "Eutzsch"
+ },
+ {
+ "ds100" : "WFEL",
+ "eva" : 8011534,
+ "latlong" : [
+ 53.330754,
+ 13.431271
+ ],
+ "name" : "Feldberg(Meckl)"
+ },
+ {
+ "ds100" : "XIFE",
+ "eva" : 8300209,
+ "latlong" : [
+ 44.843119,
+ 11.603837
+ ],
+ "name" : "Ferrara"
+ },
+ {
+ "ds100" : "MFWG",
+ "eva" : 8070601,
+ "latlong" : [
+ 49.168721,
+ 10.324017
+ ],
+ "name" : "Feuchtwangen Bf"
+ },
+ {
+ "ds100" : "XEFI",
+ "eva" : 7100078,
+ "latlong" : [
+ 42.264952,
+ 2.969276
+ ],
+ "name" : "Figueres"
+ },
+ {
+ "ds100" : "XIFLM",
+ "eva" : 8300144,
+ "latlong" : [
+ 44.169247,
+ 8.340404
+ ],
+ "name" : "Finale Ligure Marina"
+ },
+ {
+ "ds100" : "NFD",
+ "eva" : 8002001,
+ "latlong" : [
+ 50.519551,
+ 10.150691
+ ],
+ "name" : "Fladungen"
+ },
+ {
+ "ds100" : "XEFL",
+ "eva" : 7100130,
+ "latlong" : [
+ 42.047541,
+ 2.957239
+ ],
+ "name" : "Flassa"
+ },
+ {
+ "ds100" : "XIF",
+ "eva" : 8300020,
+ "latlong" : [
+ 44.550582,
+ 7.717916
+ ],
+ "name" : "Fossano"
+ },
+ {
+ "ds100" : "AFRD",
+ "eva" : 8002113,
+ "latlong" : [
+ 53.528122,
+ 10.339671
+ ],
+ "name" : "Friedrichsruh"
+ },
+ {
+ "ds100" : "LFIO",
+ "eva" : 8011587,
+ "latlong" : [
+ 51.59782,
+ 11.323432
+ ],
+ "name" : "Friesdorf Ost"
+ },
+ {
+ "ds100" : "WGAL",
+ "eva" : 8011593,
+ "latlong" : [
+ 53.516641,
+ 12.129379
+ ],
+ "name" : "Gallin"
+ },
+ {
+ "ds100" : "UGOT",
+ "eva" : 8011617,
+ "latlong" : [
+ 50.849203,
+ 12.092791
+ ],
+ "name" : "Gera Ost"
+ },
+ {
+ "ds100" : "UGLW",
+ "eva" : 8011620,
+ "latlong" : [
+ 50.838429,
+ 12.080791
+ ],
+ "name" : "Gera-Liebschwitz"
+ },
+ {
+ "ds100" : "TGSN",
+ "eva" : 8007075,
+ "latlong" : [
+ 48.624836,
+ 10.022662
+ ],
+ "name" : "Gerstetten"
+ },
+ {
+ "ds100" : "AGNBN",
+ "eva" : 8070036,
+ "latlong" : [
+ 53.393287,
+ 9.015403
+ ],
+ "name" : "Gnarrenburg Nord"
+ },
+ {
+ "ds100" : "XPGW",
+ "eva" : 5100014,
+ "latlong" : [
+ 52.727753,
+ 15.229226
+ ],
+ "name" : "Gorzow Wlkp."
+ },
+ {
+ "ds100" : "XSGL",
+ "eva" : 8506163,
+ "latlong" : [
+ 47.6646656888434,
+ 9.13467870405456
+ ],
+ "name" : "Gottlieben (Schifflände)"
+ },
+ {
+ "ds100" : "XEGR",
+ "eva" : 7100075,
+ "latlong" : [
+ 41.599307,
+ 2.291623
+ ],
+ "name" : "Granollers"
+ },
+ {
+ "ds100" : "HGIP",
+ "eva" : 8070403,
+ "latlong" : [
+ 52.949135,
+ 8.632519
+ ],
+ "name" : "Groß Ippener DHE"
+ },
+ {
+ "ds100" : "LGRK",
+ "eva" : 8011685,
+ "latlong" : [
+ 51.602149,
+ 11.404245
+ ],
+ "name" : "Gräfenstuhl-Klippmühle"
+ },
+ {
+ "ds100" : "NSDT",
+ "eva" : 8070669,
+ "latlong" : [
+ 49.064357,
+ 12.944043
+ ],
+ "name" : "Gstadt(Wanderbahn)"
+ },
+ {
+ "ds100" : "TGSS",
+ "eva" : 8007074,
+ "latlong" : [
+ 48.639901,
+ 9.957623
+ ],
+ "name" : "Gussenstadt"
+ },
+ {
+ "ds100" : "FGLD",
+ "eva" : 8070257,
+ "latlong" : [
+ 49.599376,
+ 8.016444
+ ],
+ "name" : "Göllheim-Dreisen"
+ },
+ {
+ "ds100" : "HHAD",
+ "eva" : 8002497,
+ "latlong" : [
+ 52.711982,
+ 9.639434
+ ],
+ "name" : "Hademstorf"
+ },
+ {
+ "ds100" : "AHGN",
+ "eva" : 8071337,
+ "latlong" : [
+ 53.550361,
+ 9.460078
+ ],
+ "name" : "Hagen(Kr. Stade)"
+ },
+ {
+ "ds100" : "LHG",
+ "eva" : 8098159,
+ "latlong" : [
+ 51.478461,
+ 11.987868
+ ],
+ "name" : "Halle(Saale)Hbf Gl. 13a"
+ },
+ {
+ "ds100" : "MHMB",
+ "eva" : 966904,
+ "latlong" : [
+ 47.466024,
+ 11.046595
+ ],
+ "name" : "Hammersbach Zugspitzbahn, Grainau"
+ },
+ {
+ "ds100" : "FHAX",
+ "eva" : 8070253,
+ "latlong" : [
+ 49.64074,
+ 8.137885
+ ],
+ "name" : "Harxheim-Zell"
+ },
+ {
+ "ds100" : "XMXHY",
+ "eva" : 5501629,
+ "latlong" : [
+ 47.938258,
+ 17.095845
+ ],
+ "name" : "Hegyeshalom(Gr)"
+ },
+ {
+ "ds100" : "BHEL",
+ "eva" : 8011853,
+ "latlong" : [
+ 52.278758,
+ 14.478219
+ ],
+ "name" : "Helenesee"
+ },
+ {
+ "ds100" : "AHSN",
+ "eva" : 8002750,
+ "latlong" : [
+ 53.078795,
+ 9.811174
+ ],
+ "name" : "Hemsen(b Soltau)"
+ },
+ {
+ "ds100" : "RKMH",
+ "eva" : 721376,
+ "latlong" : [
+ 49.009815,
+ 8.400056
+ ],
+ "name" : "Herrenstraße, Karlsruhe"
+ },
+ {
+ "ds100" : "FKBHK",
+ "eva" : 713942,
+ "latlong" : [
+ 51.329768,
+ 9.521922
+ ],
+ "name" : "Hinter dem Fasanenhof, Kassel"
+ },
+ {
+ "ds100" : "SHW",
+ "eva" : 8079147,
+ "latlong" : [
+ 49.203132,
+ 7.773047
+ ],
+ "name" : "Hinterweidenthal Ost"
+ },
+ {
+ "ds100" : "BHOW",
+ "eva" : 8080710,
+ "latlong" : [
+ 52.672398,
+ 13.271342
+ ],
+ "name" : "Hohen Neuendorf West"
+ },
+ {
+ "ds100" : "XSHBS",
+ "eva" : 8506111,
+ "latlong" : [
+ 47.496639074807,
+ 9.46295013675514
+ ],
+ "name" : "Horn(Bodensee), SF"
+ },
+ {
+ "ds100" : "RHFH",
+ "eva" : 8007439,
+ "latlong" : [
+ 49.291132,
+ 9.086804
+ ],
+ "name" : "Hüffenhardt"
+ },
+ {
+ "ds100" : "AHTB",
+ "eva" : 8007119,
+ "latlong" : [
+ 53.273296,
+ 8.962154
+ ],
+ "name" : "Hüttenbusch"
+ },
+ {
+ "ds100" : "FIN",
+ "eva" : 8003077,
+ "latlong" : [
+ 50.460379,
+ 8.892407
+ ],
+ "name" : "Inheiden"
+ },
+ {
+ "ds100" : "XEIR",
+ "eva" : 7100009,
+ "latlong" : [
+ 43.339403,
+ -1.801153
+ ],
+ "name" : "Irun"
+ },
+ {
+ "ds100" : "XIXIT",
+ "eva" : 8300366,
+ "latlong" : [
+ 46.187965,
+ 8.187302
+ ],
+ "name" : "Iselle transito"
+ },
+ {
+ "ds100" : "XAXRB",
+ "eva" : 7900043,
+ "latlong" : [
+ 46.481412,
+ 14.020333
+ ],
+ "name" : "Jesenice(Gr)"
+ },
+ {
+ "ds100" : "MJGS",
+ "eva" : 8003106,
+ "latlong" : [
+ 47.666782,
+ 11.087702
+ ],
+ "name" : "Jägerhaus"
+ },
+ {
+ "ds100" : "MKAI",
+ "eva" : 8003146,
+ "latlong" : [
+ 47.482981,
+ 11.116723
+ ],
+ "name" : "Kainzenbad"
+ },
+ {
+ "ds100" : "WKGW",
+ "eva" : 8011989,
+ "latlong" : [
+ 53.49952404,
+ 12.76242345
+ ],
+ "name" : "Kargow"
+ },
+ {
+ "ds100" : "XMK",
+ "eva" : 5500092,
+ "latlong" : [
+ 46.194865,
+ 19.610156
+ ],
+ "name" : "Kelebia"
+ },
+ {
+ "ds100" : "WKLE",
+ "eva" : 8012020,
+ "latlong" : [
+ 53.62316794,
+ 13.06042745
+ ],
+ "name" : "Kleeth"
+ },
+ {
+ "ds100" : "DKFR",
+ "eva" : 8017344,
+ "latlong" : [
+ 51.289921,
+ 13.104999
+ ],
+ "name" : "Kleinforst Rosensee"
+ },
+ {
+ "ds100" : "WKC",
+ "eva" : 8012049,
+ "latlong" : [
+ 53.470795,
+ 12.86687
+ ],
+ "name" : "Klockow(b Waren/Müritz)"
+ },
+ {
+ "ds100" : "LKMR",
+ "eva" : 8010204,
+ "latlong" : [
+ 51.59109,
+ 11.498511
+ ],
+ "name" : "Klostermansfeld Randsiedlung"
+ },
+ {
+ "ds100" : "LKNF",
+ "eva" : 8012059,
+ "latlong" : [
+ 51.247132,
+ 12.272945
+ ],
+ "name" : "Knautnaundorf"
+ },
+ {
+ "ds100" : "XDKG",
+ "eva" : 8601312,
+ "latlong" : [
+ 55.4575,
+ 12.186472
+ ],
+ "name" : "Koege st"
+ },
+ {
+ "ds100" : "XDKS",
+ "eva" : 8601343,
+ "latlong" : [
+ 55.357201,
+ 11.135125
+ ],
+ "name" : "Korsoer st"
+ },
+ {
+ "ds100" : "XTKO",
+ "eva" : 5401571,
+ "latlong" : [
+ 50.430178,
+ 13.035015
+ ],
+ "name" : "Kovarska"
+ },
+ {
+ "ds100" : "KKRZ",
+ "eva" : 8003436,
+ "latlong" : [
+ 50.506442,
+ 6.978557
+ ],
+ "name" : "Kreuzberg(Ahr)"
+ },
+ {
+ "ds100" : "MKZB",
+ "eva" : 966903,
+ "latlong" : [
+ 47.472102,
+ 11.062936
+ ],
+ "name" : "Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc"
+ },
+ {
+ "ds100" : "XTKM",
+ "eva" : 5401649,
+ "latlong" : [
+ 50.489755,
+ 13.276007
+ ],
+ "name" : "Krimov"
+ },
+ {
+ "ds100" : "KKDZB",
+ "eva" : 8083368,
+ "latlong" : [
+ 50.941299,
+ 6.974641
+ ],
+ "name" : "Köln Messe/Deutz Gl. 9-10"
+ },
+ {
+ "ds100" : "MLQU",
+ "eva" : 8070812,
+ "latlong" : [
+ 48.821058,
+ 12.053477
+ ],
+ "name" : "Langquaid(b Eggmühl)"
+ },
+ {
+ "ds100" : "LLSG",
+ "eva" : 8012173,
+ "latlong" : [
+ 51.540742,
+ 12.639613
+ ],
+ "name" : "Laußig(Düben)"
+ },
+ {
+ "ds100" : "XTLM",
+ "eva" : 5401791,
+ "latlong" : [
+ 50.532482,
+ 14.138833
+ ],
+ "name" : "Litomerice mesto"
+ },
+ {
+ "ds100" : "XILI",
+ "eva" : 8300157,
+ "latlong" : [
+ 43.553983,
+ 10.336626
+ ],
+ "name" : "Livorno Centrale"
+ },
+ {
+ "ds100" : "XSMMS",
+ "eva" : 8506155,
+ "latlong" : [
+ 47.6482873603292,
+ 8.91673503093438
+ ],
+ "name" : "Mammern URh"
+ },
+ {
+ "ds100" : "XSMBS",
+ "eva" : 8506160,
+ "latlong" : [
+ 47.6751705431517,
+ 9.04756474024539
+ ],
+ "name" : "Mannenbach URh"
+ },
+ {
+ "ds100" : "LMAS",
+ "eva" : 8012300,
+ "latlong" : [
+ 51.600325,
+ 11.452086
+ ],
+ "name" : "Mansfeld(Südharz)"
+ },
+ {
+ "ds100" : "DMG",
+ "eva" : 8012301,
+ "latlong" : [
+ 50.648287,
+ 13.163227
+ ],
+ "name" : "Marienberg(Sachs)"
+ },
+ {
+ "ds100" : "PQKMP",
+ "eva" : 371861,
+ "latlong" : [
+ 49.009485,
+ 8.405563
+ ],
+ "name" : "Marktplatz, Karlsruhe"
+ },
+ {
+ "ds100" : "LMDF",
+ "eva" : 8012325,
+ "latlong" : [
+ 51.725714,
+ 11.294529
+ ],
+ "name" : "Meisdorf"
+ },
+ {
+ "ds100" : "RMEN",
+ "eva" : 8077779,
+ "latlong" : [
+ 48.0067397,
+ 9.1609604
+ ],
+ "name" : "Menningen-Leitishofen"
+ },
+ {
+ "ds100" : "TNHU",
+ "eva" : 8070678,
+ "latlong" : [
+ 48.532647,
+ 9.307106
+ ],
+ "name" : "Metzingen-Neuhausen"
+ },
+ {
+ "ds100" : "FMICH",
+ "eva" : 8070165,
+ "latlong" : [
+ 50.099783,
+ 9.118298
+ ],
+ "name" : "Michelbach(Unterfr) Herrnmühle"
+ },
+ {
+ "ds100" : "XDMF",
+ "eva" : 8601593,
+ "latlong" : [
+ 55.501623,
+ 9.734662
+ ],
+ "name" : "Middelfart st"
+ },
+ {
+ "ds100" : "XTMHN",
+ "eva" : 5401976,
+ "latlong" : [
+ 50.956468,
+ 14.390211
+ ],
+ "name" : "Mikulasovice horni nadrazi"
+ },
+ {
+ "ds100" : "XTMHS",
+ "eva" : 5401977,
+ "latlong" : [
+ 50.962356,
+ 14.357535
+ ],
+ "name" : "Mikulasovice stred"
+ },
+ {
+ "ds100" : "ZWM",
+ "eva" : 2100003,
+ "latlong" : [
+ 53.890736,
+ 27.55064
+ ],
+ "name" : "Minsk-Passajirskii"
+ },
+ {
+ "ds100" : "XSMOS",
+ "eva" : 8509415,
+ "latlong" : [
+ 47.112987492662,
+ 9.27680343664119
+ ],
+ "name" : "Mols"
+ },
+ {
+ "ds100" : "SMBT",
+ "eva" : 8079265,
+ "latlong" : [
+ 49.165954,
+ 7.754959
+ ],
+ "name" : "Moosbachtal"
+ },
+ {
+ "ds100" : "XCM",
+ "eva" : 2000058,
+ "latlong" : [
+ 55.776672,
+ 37.57981
+ ],
+ "name" : "Moskva Belorusskaja"
+ },
+ {
+ "ds100" : "DMUES",
+ "eva" : 8070696,
+ "latlong" : [
+ 51.238593,
+ 13.047845
+ ],
+ "name" : "Mügeln Stadt"
+ },
+ {
+ "ds100" : "TMRN",
+ "eva" : 8070494,
+ "latlong" : [
+ 48.420328,
+ 8.760169
+ ],
+ "name" : "Mühringen"
+ },
+ {
+ "ds100" : "MS",
+ "eva" : 8099501,
+ "latlong" : [
+ 48.12309,
+ 11.551565
+ ],
+ "name" : "München-Süd"
+ },
+ {
+ "ds100" : "XDNV",
+ "eva" : 8601645,
+ "latlong" : [
+ 55.231875,
+ 11.766926
+ ],
+ "name" : "Naestved st"
+ },
+ {
+ "ds100" : "DNAO",
+ "eva" : 8070691,
+ "latlong" : [
+ 51.25978,
+ 13.101472
+ ],
+ "name" : "Naundorf (b Oschatz)"
+ },
+ {
+ "ds100" : "RNHS",
+ "eva" : 8007434,
+ "latlong" : [
+ 49.296035,
+ 8.963686
+ ],
+ "name" : "Neckarbischofsheim Stadt"
+ },
+ {
+ "ds100" : "D730823",
+ "eva" : 730823,
+ "latlong" : [
+ 52.544431,
+ 13.370133
+ ],
+ "name" : "Nettelbeckplatz/Wedding (S), Berlin"
+ },
+ {
+ "ds100" : "HNPL",
+ "eva" : 8004260,
+ "latlong" : [
+ 52.548702,
+ 10.60754
+ ],
+ "name" : "Neudorf-Platendorf"
+ },
+ {
+ "ds100" : "MNHL",
+ "eva" : 8026358,
+ "latlong" : [
+ 48.618764,
+ 11.996039
+ ],
+ "name" : "Neuhausen(b Landshut)"
+ },
+ {
+ "ds100" : "WNRS",
+ "eva" : 8080175,
+ "latlong" : [
+ 52.92535,
+ 12.82751
+ ],
+ "name" : "Neuruppin Seedamm"
+ },
+ {
+ "ds100" : "XDNP",
+ "eva" : 8601699,
+ "latlong" : [
+ 55.683522,
+ 12.571867
+ ],
+ "name" : "Noerreport st"
+ },
+ {
+ "ds100" : "ANDS",
+ "eva" : 8007116,
+ "latlong" : [
+ 53.332837,
+ 8.980288
+ ],
+ "name" : "Nordsode"
+ },
+ {
+ "ds100" : "XJNB",
+ "eva" : 7200210,
+ "latlong" : [
+ 44.806916,
+ 20.418098
+ ],
+ "name" : "Novi Beograd"
+ },
+ {
+ "ds100" : "XJNS",
+ "eva" : 7200008,
+ "latlong" : [
+ 45.2657352,
+ 19.8292601
+ ],
+ "name" : "Novi Sad"
+ },
+ {
+ "ds100" : "NNBS",
+ "eva" : 8070668,
+ "latlong" : [
+ 49.070682,
+ 12.965009
+ ],
+ "name" : "Nußberg-Schönau"
+ },
+ {
+ "ds100" : "XDNE",
+ "eva" : 8613687,
+ "latlong" : [
+ 55.652421,
+ 12.516317
+ ],
+ "name" : "Ny Ellebjerg st"
+ },
+ {
+ "ds100" : "XDNY",
+ "eva" : 8601739,
+ "latlong" : [
+ 55.314068,
+ 10.802921
+ ],
+ "name" : "Nyborg st"
+ },
+ {
+ "ds100" : "XDNK",
+ "eva" : 8601745,
+ "latlong" : [
+ 54.766833,
+ 11.877767
+ ],
+ "name" : "Nykoebing F st"
+ },
+ {
+ "ds100" : "NND",
+ "eva" : 8098493,
+ "latlong" : [
+ 49.431761,
+ 11.127479
+ ],
+ "name" : "Nürnberg Frankenstadion Sonderbahnsteig"
+ },
+ {
+ "ds100" : "ROGI",
+ "eva" : 8007437,
+ "latlong" : [
+ 49.255404,
+ 9.044019
+ ],
+ "name" : "Obergimpern"
+ },
+ {
+ "ds100" : "UOF",
+ "eva" : 8012525,
+ "latlong" : [
+ 50.684213,
+ 10.709405
+ ],
+ "name" : "Oberhof(Thür)"
+ },
+ {
+ "ds100" : "MOBG",
+ "eva" : 8070803,
+ "latlong" : [
+ 47.998779,
+ 12.403156
+ ],
+ "name" : "Obing"
+ },
+ {
+ "ds100" : "XDKHO",
+ "eva" : 8601560,
+ "latlong" : [
+ 55.6290128,
+ 12.579355
+ ],
+ "name" : "Oerestad st"
+ },
+ {
+ "ds100" : "EOES",
+ "eva" : 8004631,
+ "latlong" : [
+ 51.402051,
+ 7.788853
+ ],
+ "name" : "Oese"
+ },
+ {
+ "ds100" : "XDOP",
+ "eva" : 8601878,
+ "latlong" : [
+ 55.692708,
+ 12.587615
+ ],
+ "name" : "Oesterport st"
+ },
+ {
+ "ds100" : "XROK",
+ "eva" : 7800036,
+ "latlong" : [
+ 45.2524891,
+ 17.2063555
+ ],
+ "name" : "Okucani"
+ },
+ {
+ "ds100" : "ZWO",
+ "eva" : 2100012,
+ "latlong" : [
+ 54.520839,
+ 30.374882
+ ],
+ "name" : "Orscha Central"
+ },
+ {
+ "ds100" : "XIOV",
+ "eva" : 8300257,
+ "latlong" : [
+ 42.72393,
+ 12.126752
+ ],
+ "name" : "Orvieto"
+ },
+ {
+ "ds100" : "DOT K",
+ "eva" : 8070686,
+ "latlong" : [
+ 51.300102,
+ 13.111648
+ ],
+ "name" : "Oschatz Körnerstr"
+ },
+ {
+ "ds100" : "DOT L",
+ "eva" : 8070685,
+ "latlong" : [
+ 51.302517,
+ 13.109548
+ ],
+ "name" : "Oschatz Lichtstr"
+ },
+ {
+ "ds100" : "DOTS",
+ "eva" : 8070688,
+ "latlong" : [
+ 51.294173,
+ 13.110174
+ ],
+ "name" : "Oschatz Südbf"
+ },
+ {
+ "ds100" : "NOMR",
+ "eva" : 8004711,
+ "latlong" : [
+ 50.455926,
+ 10.231621
+ ],
+ "name" : "Ostheim v Rhön"
+ },
+ {
+ "ds100" : "XPOM",
+ "eva" : 5100048,
+ "latlong" : [
+ 50.041503,
+ 19.199724
+ ],
+ "name" : "Oswiecim"
+ },
+ {
+ "ds100" : "XTPAN",
+ "eva" : 5402315,
+ "latlong" : [
+ 50.948252,
+ 14.463796
+ ],
+ "name" : "Pansky"
+ },
+ {
+ "ds100" : "WPAW",
+ "eva" : 8012612,
+ "latlong" : [
+ 53.50811,
+ 12.054112
+ ],
+ "name" : "Passow(Meckl)"
+ },
+ {
+ "ds100" : "MPFT",
+ "eva" : 8026355,
+ "latlong" : [
+ 48.574499,
+ 12.066113
+ ],
+ "name" : "Pfettrach"
+ },
+ {
+ "ds100" : "XSXPT",
+ "eva" : 8505409,
+ "latlong" : [
+ 46.1045508232004,
+ 8.7570199823613
+ ],
+ "name" : "Pino transito"
+ },
+ {
+ "ds100" : "XIPI",
+ "eva" : 8300169,
+ "latlong" : [
+ 43.707973,
+ 10.398174
+ ],
+ "name" : "Pisa Centrale"
+ },
+ {
+ "ds100" : "MPTH",
+ "eva" : 8072764,
+ "latlong" : [
+ 47.986762,
+ 12.376341
+ ],
+ "name" : "Pittenhart"
+ },
+ {
+ "ds100" : "DPU",
+ "eva" : 8013463,
+ "latlong" : [
+ 50.48624,
+ 12.129382
+ ],
+ "name" : "Plauen(V) unt Bf"
+ },
+ {
+ "ds100" : "WPRH",
+ "eva" : 8074703,
+ "latlong" : [
+ 53.170082,
+ 12.20592
+ ],
+ "name" : "Pritzwalk Hainholz"
+ },
+ {
+ "ds100" : "APSH",
+ "eva" : 8007318,
+ "latlong" : [
+ 54.365139,
+ 10.292991
+ ],
+ "name" : "Probsteierhagen"
+ },
+ {
+ "ds100" : "NPM",
+ "eva" : 8070858,
+ "latlong" : [
+ 49.864518,
+ 10.123567
+ ],
+ "name" : "Prosselsheim"
+ },
+ {
+ "ds100" : "ERAE",
+ "eva" : 8004918,
+ "latlong" : [
+ 51.965867,
+ 7.867547
+ ],
+ "name" : "Raestrup-Everswinkel"
+ },
+ {
+ "ds100" : "KREC",
+ "eva" : 8004967,
+ "latlong" : [
+ 50.515427,
+ 7.036514
+ ],
+ "name" : "Rech"
+ },
+ {
+ "ds100" : "MRFR",
+ "eva" : 966907,
+ "latlong" : [
+ 47.43554,
+ 10.987337
+ ],
+ "name" : "Riffelriß, Grainau"
+ },
+ {
+ "ds100" : "XIRI",
+ "eva" : 8300221,
+ "latlong" : [
+ 44.064237,
+ 12.574016
+ ],
+ "name" : "Rimini"
+ },
+ {
+ "ds100" : "XDR",
+ "eva" : 8601988,
+ "latlong" : [
+ 54.655717,
+ 11.357463
+ ],
+ "name" : "Roedby"
+ },
+ {
+ "ds100" : "XSRHF",
+ "eva" : 8506112,
+ "latlong" : [
+ 47.5655617030878,
+ 9.37997531046235
+ ],
+ "name" : "Romanshorn (See)"
+ },
+ {
+ "ds100" : "XSRSS",
+ "eva" : 8506113,
+ "latlong" : [
+ 47.4789070232403,
+ 9.49250636652451
+ ],
+ "name" : "Rorschach Hafen (See)"
+ },
+ {
+ "ds100" : "MROS",
+ "eva" : 8005173,
+ "latlong" : [
+ 47.866304,
+ 12.104178
+ ],
+ "name" : "Rosenheim Hochschule"
+ },
+ {
+ "ds100" : "XDRK",
+ "eva" : 8602026,
+ "latlong" : [
+ 55.639014,
+ 12.088854
+ ],
+ "name" : "Roskilde st"
+ },
+ {
+ "ds100" : "XNRS",
+ "eva" : 8400015,
+ "latlong" : [
+ 51.893890380859,
+ 4.5197219848633
+ ],
+ "name" : "Rotterdam Stadion"
+ },
+ {
+ "ds100" : "XIRG",
+ "eva" : 8300238,
+ "latlong" : [
+ 45.076985,
+ 11.781064
+ ],
+ "name" : "Rovigo"
+ },
+ {
+ "ds100" : "TROS",
+ "eva" : 8005174,
+ "latlong" : [
+ 47.86541789,
+ 9.78662425
+ ],
+ "name" : "Roßberg"
+ },
+ {
+ "ds100" : "XJRU",
+ "eva" : 7200010,
+ "latlong" : [
+ 44.9895547,
+ 19.8270339
+ ],
+ "name" : "Ruma"
+ },
+ {
+ "ds100" : "XTRS",
+ "eva" : 5402723,
+ "latlong" : [
+ 50.450027,
+ 13.169033
+ ],
+ "name" : "Rusova"
+ },
+ {
+ "ds100" : "XFRF",
+ "eva" : 8701876,
+ "latlong" : [
+ 49.091525,
+ 7.091677
+ ],
+ "name" : "Rémelfing"
+ },
+ {
+ "ds100" : "RRMM",
+ "eva" : 8070524,
+ "latlong" : [
+ 47.642926,
+ 7.640709
+ ],
+ "name" : "Rümmingen"
+ },
+ {
+ "ds100" : "XISAR",
+ "eva" : 8300182,
+ "latlong" : [
+ 43.829038,
+ 7.784346
+ ],
+ "name" : "San Remo"
+ },
+ {
+ "ds100" : "HSAB",
+ "eva" : 8005285,
+ "latlong" : [
+ 53.507247,
+ 8.014777
+ ],
+ "name" : "Sanderbusch"
+ },
+ {
+ "ds100" : "XRXSJ",
+ "eva" : 7800076,
+ "latlong" : [
+ 45.505158,
+ 14.244213
+ ],
+ "name" : "Sapjane(Gr)"
+ },
+ {
+ "ds100" : "XNSO",
+ "eva" : 8400545,
+ "latlong" : [
+ 53.1589183,
+ 6.7955648
+ ],
+ "name" : "Sappemeer Oost"
+ },
+ {
+ "ds100" : "XFSI",
+ "eva" : 8700528,
+ "latlong" : [
+ 49.086193,
+ 7.111315
+ ],
+ "name" : "Sarreinsming"
+ },
+ {
+ "ds100" : "RSDO",
+ "eva" : 8077776,
+ "latlong" : [
+ 47.944482,
+ 9.0946518
+ ],
+ "name" : "Sauldorf"
+ },
+ {
+ "ds100" : "XISAV",
+ "eva" : 8300185,
+ "latlong" : [
+ 44.307276,
+ 8.470105
+ ],
+ "name" : "Savona"
+ },
+ {
+ "ds100" : "TSKS",
+ "eva" : 8007072,
+ "latlong" : [
+ 48.611798,
+ 9.91133
+ ],
+ "name" : "Schalkstetten"
+ },
+ {
+ "ds100" : "MSC",
+ "eva" : 8005327,
+ "latlong" : [
+ 47.927051,
+ 12.126724
+ ],
+ "name" : "Schechen"
+ },
+ {
+ "ds100" : "MSLG",
+ "eva" : 8070811,
+ "latlong" : [
+ 48.831851,
+ 12.142663
+ ],
+ "name" : "Schierling"
+ },
+ {
+ "ds100" : "DSWO",
+ "eva" : 8070692,
+ "latlong" : [
+ 51.245226,
+ 13.084558
+ ],
+ "name" : "Schweta Gasth"
+ },
+ {
+ "ds100" : "TSCS",
+ "eva" : 8072211,
+ "latlong" : [
+ 48.21129,
+ 8.771335
+ ],
+ "name" : "Schömberg Stausee"
+ },
+ {
+ "ds100" : "TSCB",
+ "eva" : 8029359,
+ "latlong" : [
+ 48.205743,
+ 8.758503
+ ],
+ "name" : "Schömberg(b Balingen)"
+ },
+ {
+ "ds100" : "ASCK",
+ "eva" : 8007314,
+ "latlong" : [
+ 54.332873,
+ 10.229329
+ ],
+ "name" : "Schönkirchen Bf"
+ },
+ {
+ "ds100" : "XTSBZ",
+ "eva" : 5402772,
+ "latlong" : [
+ 50.593048,
+ 14.053958
+ ],
+ "name" : "Sebuzin"
+ },
+ {
+ "ds100" : "XJSI",
+ "eva" : 7200140,
+ "latlong" : [
+ 45.117524,
+ 19.221557
+ ],
+ "name" : "Sid(SRB)"
+ },
+ {
+ "ds100" : "RSGB",
+ "eva" : 8007438,
+ "latlong" : [
+ 49.271227,
+ 9.087386
+ ],
+ "name" : "Siegelsbach"
+ },
+ {
+ "ds100" : "LSIE",
+ "eva" : 8012992,
+ "latlong" : [
+ 52.818929,
+ 12.397722
+ ],
+ "name" : "Sieversdorf(Neust/D)"
+ },
+ {
+ "ds100" : "XUSI",
+ "eva" : 5300003,
+ "latlong" : [
+ 45.846272,
+ 23.013784
+ ],
+ "name" : "Simeria"
+ },
+ {
+ "ds100" : "XZSL",
+ "eva" : 7900048,
+ "latlong" : [
+ 46.1743369,
+ 14.3356158
+ ],
+ "name" : "Skofja Loka"
+ },
+ {
+ "ds100" : "XDSGE",
+ "eva" : 8602254,
+ "latlong" : [
+ 55.407553,
+ 11.348057
+ ],
+ "name" : "Slagelse st"
+ },
+ {
+ "ds100" : "XCSM",
+ "eva" : 2000004,
+ "latlong" : [
+ 54.798343,
+ 32.034466
+ ],
+ "name" : "Smolensk"
+ },
+ {
+ "ds100" : "XDSOR",
+ "eva" : 8602350,
+ "latlong" : [
+ 55.419019,
+ 11.568794
+ ],
+ "name" : "Soroe st"
+ },
+ {
+ "ds100" : "XJSM",
+ "eva" : 7200011,
+ "latlong" : [
+ 44.9823572,
+ 19.6136963
+ ],
+ "name" : "Sremska Mitrovica"
+ },
+ {
+ "ds100" : "XFSHT",
+ "eva" : 8701972,
+ "latlong" : [
+ 49.055928,
+ 4.379409
+ ],
+ "name" : "St-Hilaire-au-Temple"
+ },
+ {
+ "ds100" : "XJSP",
+ "eva" : 7200174,
+ "latlong" : [
+ 44.98629,
+ 20.138006
+ ],
+ "name" : "Stara Pazova"
+ },
+ {
+ "ds100" : "XTSKR",
+ "eva" : 5402921,
+ "latlong" : [
+ 50.947794,
+ 14.491942
+ ],
+ "name" : "Stare Krecany"
+ },
+ {
+ "ds100" : "XSSBS",
+ "eva" : 8506157,
+ "latlong" : [
+ 47.6686469246848,
+ 8.98143540966547
+ ],
+ "name" : "Steckborn URh"
+ },
+ {
+ "ds100" : "NSTW",
+ "eva" : 8005716,
+ "latlong" : [
+ 50.292441,
+ 11.462246
+ ],
+ "name" : "Steinwiesen Bf"
+ },
+ {
+ "ds100" : "TSTT",
+ "eva" : 8070548,
+ "latlong" : [
+ 48.353415,
+ 8.812741
+ ],
+ "name" : "Stetten (b. Haigerloch)"
+ },
+ {
+ "ds100" : "KST G",
+ "eva" : 8099506,
+ "latlong" : [
+ 50.797555,
+ 6.22488
+ ],
+ "name" : "Stolberg(Rheinl)Gbf"
+ },
+ {
+ "ds100" : "TSBH",
+ "eva" : 8007071,
+ "latlong" : [
+ 48.593792,
+ 9.920377
+ ],
+ "name" : "Stubersheim"
+ },
+ {
+ "ds100" : "XJST",
+ "eva" : 7200012,
+ "latlong" : [
+ 46.102069,
+ 19.671131
+ ],
+ "name" : "Subotica"
+ },
+ {
+ "ds100" : "XMXSB",
+ "eva" : 5603743,
+ "latlong" : [
+ 47.824896,
+ 18.844774
+ ],
+ "name" : "Szob(Gr)"
+ },
+ {
+ "ds100" : "LSOL",
+ "eva" : 8013001,
+ "latlong" : [
+ 51.633456,
+ 12.654031
+ ],
+ "name" : "Söllichau"
+ },
+ {
+ "ds100" : "XITG",
+ "eva" : 8300176,
+ "latlong" : [
+ 43.844625,
+ 7.855336
+ ],
+ "name" : "Taggia"
+ },
+ {
+ "ds100" : "XDTA",
+ "eva" : 8602526,
+ "latlong" : [
+ 55.545016,
+ 9.61571
+ ],
+ "name" : "Taulov st"
+ },
+ {
+ "ds100" : "XPTE",
+ "eva" : 5100084,
+ "latlong" : [
+ 52.07414,
+ 23.601227
+ ],
+ "name" : "Terespol"
+ },
+ {
+ "ds100" : "NTFB",
+ "eva" : 8026544,
+ "latlong" : [
+ 48.61502,
+ 13.42308
+ ],
+ "name" : "Tiefenbach(b Passau)"
+ },
+ {
+ "ds100" : "XDTP",
+ "eva" : 8602588,
+ "latlong" : [
+ 55.349752,
+ 10.182602
+ ],
+ "name" : "Tommerup st"
+ },
+ {
+ "ds100" : "XRT",
+ "eva" : 7800197,
+ "latlong" : [
+ 45.155272,
+ 19.148794
+ ],
+ "name" : "Tovarnik"
+ },
+ {
+ "ds100" : "LTRB",
+ "eva" : 8013140,
+ "latlong" : [
+ 51.704991,
+ 11.763092
+ ],
+ "name" : "Trebitz(Könnern)"
+ },
+ {
+ "ds100" : "XVTF",
+ "eva" : 7400795,
+ "latlong" : [
+ 55.372579,
+ 13.151892
+ ],
+ "name" : "Trelleborg F"
+ },
+ {
+ "ds100" : "XATK",
+ "eva" : 8100379,
+ "latlong" : [
+ 48.027377,
+ 12.860483
+ ],
+ "name" : "Trimmelkam"
+ },
+ {
+ "ds100" : "UTRO",
+ "eva" : 8013152,
+ "latlong" : [
+ 51.118495,
+ 11.481268
+ ],
+ "name" : "Tromsdorf"
+ },
+ {
+ "ds100" : "RKTUT",
+ "eva" : 720995,
+ "latlong" : [
+ 49.006769,
+ 8.431264
+ ],
+ "name" : "Tullastraße/Verkehrsbetriebe, Karlsruhe"
+ },
+ {
+ "ds100" : "XPTY",
+ "eva" : 5100260,
+ "latlong" : [
+ 50.136194,
+ 18.964092
+ ],
+ "name" : "Tychy"
+ },
+ {
+ "ds100" : "MURB",
+ "eva" : 8005961,
+ "latlong" : [
+ 47.818772,
+ 12.320636
+ ],
+ "name" : "Umrathshausen Bf"
+ },
+ {
+ "ds100" : "XDVAL",
+ "eva" : 8602714,
+ "latlong" : [
+ 55.663794,
+ 12.514324
+ ],
+ "name" : "Valby(Koebenhavn)"
+ },
+ {
+ "ds100" : "LVT",
+ "eva" : 8013182,
+ "latlong" : [
+ 51.597687,
+ 11.428993
+ ],
+ "name" : "Vatterode"
+ },
+ {
+ "ds100" : "LVTT",
+ "eva" : 8079152,
+ "latlong" : [
+ 51.598218,
+ 11.412188
+ ],
+ "name" : "Vatteröder Teich"
+ },
+ {
+ "ds100" : "XTVY",
+ "eva" : 5400383,
+ "latlong" : [
+ 50.500192,
+ 13.033743
+ ],
+ "name" : "Vejprty"
+ },
+ {
+ "ds100" : "XTVZ",
+ "eva" : 5403344,
+ "latlong" : [
+ 50.526096,
+ 14.080554
+ ],
+ "name" : "Velke Zernoseky"
+ },
+ {
+ "ds100" : "XDVSJ",
+ "eva" : 8602825,
+ "latlong" : [
+ 55.544854,
+ 12.02755
+ ],
+ "name" : "Viby Sjaelland st"
+ },
+ {
+ "ds100" : "XFVT",
+ "eva" : 8700520,
+ "latlong" : [
+ 48.202506,
+ 5.941865
+ ],
+ "name" : "Vittel"
+ },
+ {
+ "ds100" : "XIVOG",
+ "eva" : 8300492,
+ "latlong" : [
+ 44.997908,
+ 9.008735
+ ],
+ "name" : "Voghera"
+ },
+ {
+ "ds100" : "NVOLA",
+ "eva" : 8070860,
+ "latlong" : [
+ 49.864345,
+ 10.217033
+ ],
+ "name" : "Volkach-Astheim"
+ },
+ {
+ "ds100" : "XDVB",
+ "eva" : 8602903,
+ "latlong" : [
+ 55.012519,
+ 11.899413
+ ],
+ "name" : "Vordingborg st"
+ },
+ {
+ "ds100" : "FWAM",
+ "eva" : 8006126,
+ "latlong" : [
+ 49.635362,
+ 8.168439
+ ],
+ "name" : "Wachenheim-Mölsheim"
+ },
+ {
+ "ds100" : "TWHS",
+ "eva" : 8007073,
+ "latlong" : [
+ 48.635308,
+ 9.907723
+ ],
+ "name" : "Waldhausen(b Geislingen)"
+ },
+ {
+ "ds100" : "OFWL",
+ "eva" : 8702265,
+ "latlong" : [
+ 49.22391,
+ 6.15963
+ ],
+ "name" : "Walygator Parc"
+ },
+ {
+ "ds100" : "RWZ",
+ "eva" : 8070569,
+ "latlong" : [
+ 47.768832,
+ 8.476514
+ ],
+ "name" : "Weizen"
+ },
+ {
+ "ds100" : "EWGO",
+ "eva" : 8070571,
+ "latlong" : [
+ 51.40007591,
+ 7.35000344
+ ],
+ "name" : "Wengern Ost"
+ },
+ {
+ "ds100" : "MWBN",
+ "eva" : 8070620,
+ "latlong" : [
+ 49.025162,
+ 10.397122
+ ],
+ "name" : "Wilburgstetten Bf"
+ },
+ {
+ "ds100" : "FSY",
+ "eva" : 8005762,
+ "latlong" : [
+ 51.282472,
+ 8.629398
+ ],
+ "name" : "Willingen-Stryck"
+ },
+ {
+ "ds100" : "XLWW",
+ "eva" : 8271120,
+ "latlong" : [
+ 49.988201,
+ 6.00081
+ ],
+ "name" : "Wilwerwiltz"
+ },
+ {
+ "ds100" : "XPWIT",
+ "eva" : 5100261,
+ "latlong" : [
+ 52.667433,
+ 14.896236
+ ],
+ "name" : "Witnica"
+ },
+ {
+ "ds100" : "XFWT",
+ "eva" : 8702282,
+ "latlong" : [
+ 49.056011,
+ 7.140685
+ ],
+ "name" : "Wittring"
+ },
+ {
+ "ds100" : "XCV",
+ "eva" : 2000021,
+ "latlong" : [
+ 55.197393,
+ 34.317551
+ ],
+ "name" : "Wjasma"
+ },
+ {
+ "ds100" : "XPZD",
+ "eva" : 5100264,
+ "latlong" : [
+ 49.870492,
+ 18.623698
+ ],
+ "name" : "Zebrzydowice"
+ },
+ {
+ "ds100" : "XFZ",
+ "eva" : 8702295,
+ "latlong" : [
+ 49.078197,
+ 7.134154
+ ],
+ "name" : "Zetting"
+ },
+ {
+ "ds100" : "XEZM",
+ "eva" : 7100369,
+ "latlong" : [
+ 43.087052,
+ -2.320028
+ ],
+ "name" : "Zumarraga"
+ }
+]
diff --git a/t/01-static.t b/t/01-static.t
index 1d80ee0..3727f1e 100644
--- a/t/01-static.t
+++ b/t/01-static.t
@@ -1,4 +1,9 @@
#!/usr/bin/env perl
+
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
use Mojo::Base -strict;
use Test::More;
diff --git a/t/02-registration.t b/t/02-registration.t
index 2258d9a..799022f 100644
--- a/t/02-registration.t
+++ b/t/02-registration.t
@@ -1,4 +1,9 @@
#!/usr/bin/env perl
+
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
use Mojo::Base -strict;
# Tests the standard registration -> verification -> successful login flow
@@ -28,6 +33,7 @@ $t->app->pg->on(
$t->app->config->{mail}->{disabled} = 1;
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1;
$t->app->start( 'database', 'migrate' );
my $csrf_token
@@ -38,6 +44,7 @@ my $csrf_token
$t->post_ok(
'/register' => form => {
csrf_token => $csrf_token,
+ dt => 1,
user => 'someone',
email => 'foo@example.org',
password => 'foofoofoo',
@@ -46,10 +53,23 @@ $t->post_ok(
);
$t->status_is(200)->content_like(qr{Verifizierungslink});
+# Failed registration (CSRF)
+$t->post_ok(
+ '/register' => form => {
+ csrf_token => $csrf_token,
+ user => 'noone',
+ email => 'foo2@example.org',
+ password => 'foofoofoo',
+ password2 => 'foofoofoo',
+ }
+);
+$t->status_is(400)->content_like(qr{CSRF});
+
# Failed registration (user name not available)
$t->post_ok(
'/register' => form => {
csrf_token => $csrf_token,
+ dt => 1,
user => 'someone',
email => 'foo@example.org',
password => 'foofoofoo',
@@ -69,7 +89,7 @@ $t->post_ok(
password => 'foofoofoo',
}
);
-$t->status_is(200)->content_like(qr{nicht freigeschaltet});
+$t->status_is(400)->content_like(qr{nicht freigeschaltet});
my $res = $t->app->pg->db->select( 'users', ['id'], { name => 'someone' } );
my $uid = $res->hash->{id};
@@ -89,7 +109,7 @@ $t->post_ok(
password => 'definitely invalid',
}
);
-$t->status_is(200)->content_like(qr{falsches Passwort});
+$t->status_is(400)->content_like(qr{falsches Passwort});
# Successful login
$t->post_ok(
@@ -201,105 +221,5 @@ $t->post_ok(
);
$t->status_is(302)->header_is( location => '/account' );
-$csrf_token
- = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
- ->attr('value');
-$t->post_ok(
- '/journey/add' => form => {
- csrf_token => $csrf_token,
- action => 'save',
- train => 'RE 42 11238',
- dep_station => 'EMST',
- sched_departure => '16.10.2018 17:36',
- rt_departure => '16.10.2018 17:36',
- arr_station => 'EG',
- sched_arrival => '16.10.2018 18:34',
- rt_arrival => '16.10.2018 18: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})
- ->content_like(qr{Passierschein A38});
-
-$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})
- ->content_like(qr{Bei Abfahrt: 00:00 Stunden})
- ->content_like(qr{Bei Ankunft: 00:00 Stunden});
-
-$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'\]});
-
-$csrf_token
- = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
- ->attr('value');
-$t->post_ok(
- '/journey/add' => form => {
- csrf_token => $csrf_token,
- action => 'save',
- train => 'RE 42 11238',
- dep_station => 'EMST',
- sched_departure => '16.11.2018 17:36',
- rt_departure => '16.11.2018 17:45',
- arr_station => 'EG',
- sched_arrival => '16.11.2018 18:34',
- rt_arrival => '16.11.2018 19: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})
- ->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})
- ->content_like(qr{00:00 Stunden})
- ->content_like(qr{Bei Abfahrt: 00:09 Stunden})
- ->content_like(qr{Bei Ankunft: 00:26 Stunden});
-
-$csrf_token
- = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
- ->attr('value');
-$t->post_ok(
- '/journey/add' => form => {
- csrf_token => $csrf_token,
- action => 'save',
- train => 'ICE 1',
- dep_station => 'EE',
- sched_departure => '17.11.2018 15:42',
- rt_departure => '',
- arr_station => 'BL',
- sched_arrival => '17.11.2018 19: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})
- ->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})
- ->content_like(qr{00:00 Stunden})
- ->content_like(qr{Bei Abfahrt: 00:09 Stunden})
- ->content_like(qr{Bei Ankunft: 00:26 Stunden});
-
$t->app->pg->db->query('drop schema travelynx_test_02 cascade');
done_testing();
diff --git a/t/11-journey-stats.t b/t/11-journey-stats.t
new file mode 100644
index 0000000..9853b85
--- /dev/null
+++ b/t/11-journey-stats.t
@@ -0,0 +1,178 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_11 cascade');
+$t->app->pg->db->query('create schema travelynx_test_11');
+$t->app->pg->db->query('set search_path to travelynx_test_11');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_11');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
+$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 => '/' );
+
+$csrf_token
+ = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+$t->post_ok(
+ '/journey/add' => form => {
+ csrf_token => $csrf_token,
+ action => 'save',
+ train => 'RE 42 11238',
+ dep_station => 'EMST',
+ sched_departure => '16.10.2018 17:36',
+ rt_departure => '16.10.2018 17:36',
+ arr_station => 'EG',
+ sched_arrival => '16.10.2018 18:34',
+ rt_arrival => '16.10.2018 18: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})
+ ->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})
+ ->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})
+ ->content_like(qr{Bei Abfahrt: 00:00 Stunden})
+ ->content_like(qr{Bei Ankunft: 00:00 Stunden});
+
+$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'\]});
+
+$csrf_token
+ = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+$t->post_ok(
+ '/journey/add' => form => {
+ csrf_token => $csrf_token,
+ action => 'save',
+ train => 'RE 42 11238',
+ dep_station => 'EMST',
+ sched_departure => '16.11.2018 17:36',
+ rt_departure => '16.11.2018 17:45',
+ arr_station => 'EG',
+ sched_arrival => '16.11.2018 18:34',
+ rt_arrival => '16.11.2018 19: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})
+ ->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})
+ ->content_like(qr{00:00 Stunden})
+ ->content_like(qr{Bei Abfahrt: 00:09 Stunden})
+ ->content_like(qr{Bei Ankunft: 00:26 Stunden});
+
+$csrf_token
+ = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+$t->post_ok(
+ '/journey/add' => form => {
+ csrf_token => $csrf_token,
+ action => 'save',
+ train => 'ICE 1',
+ dep_station => 'EE',
+ sched_departure => '17.11.2018 15:42',
+ rt_departure => '',
+ arr_station => 'BL',
+ sched_arrival => '17.11.2018 19: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})
+ ->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})
+ ->content_like(qr{00:00 Stunden})
+ ->content_like(qr{Bei Abfahrt: 00:09 Stunden})
+ ->content_like(qr{Bei Ankunft: 00:26 Stunden});
+
+$t->app->pg->db->query('drop schema travelynx_test_11 cascade');
+done_testing();
diff --git a/t/12-journey-edit.t b/t/12-journey-edit.t
new file mode 100644
index 0000000..27e309b
--- /dev/null
+++ b/t/12-journey-edit.t
@@ -0,0 +1,206 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+use DateTime;
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_12 cascade');
+$t->app->pg->db->query('create schema travelynx_test_12');
+$t->app->pg->db->query('set search_path to travelynx_test_12');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_12');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
+$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 => '/' );
+
+$t->app->journeys->add(
+ db => $t->app->pg->db,
+ uid => $uid,
+ dep_station => 'EMST',
+ arr_station => 'EG',
+ sched_departure => DateTime->new(
+ year => 2018,
+ month => 10,
+ day => 16,
+ hour => 17,
+ minute => 36,
+ time_zone => 'Europe/Berlin'
+ ),
+ rt_departure => DateTime->new(
+ year => 2018,
+ month => 10,
+ day => 16,
+ hour => 17,
+ minute => 36,
+ time_zone => 'Europe/Berlin'
+ ),
+ sched_arrival => DateTime->new(
+ year => 2018,
+ month => 10,
+ day => 16,
+ hour => 18,
+ minute => 34,
+ time_zone => 'Europe/Berlin'
+ ),
+ rt_arrival => DateTime->new(
+ year => 2018,
+ month => 10,
+ day => 16,
+ hour => 18,
+ minute => 34,
+ time_zone => 'Europe/Berlin'
+ ),
+ cancelled => 0,
+ train_type => 'RE',
+ train_line => '42',
+ train_no => '11238',
+ comment => 'Huhu'
+);
+
+$t->get_ok('/journey/1')->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
+ ->content_like(qr{Linie 42})->content_like(qr{..:36})
+ ->content_like(qr{..:34})->content_like(qr{ca[.] 62 km})
+ ->content_like(qr{Luftlinie: 62 km})->content_like(qr{64 km/h})
+ ->content_like(qr{Huhu})->content_like(qr{Daten wurden manuell eingetragen});
+
+$t->post_ok(
+ '/journey/edit' => form => {
+ action => 'edit',
+ journey_id => 1,
+ }
+);
+
+$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');
+
+$t->post_ok(
+ '/journey/edit' => form => {
+ action => 'save',
+ journey_id => 1,
+ csrf_token => $csrf_token,
+ from_name => 'Münster(Westf)Hbf',
+ to_name => 'Gelsenkirchen Hbf',
+ sched_departure => '16.10.2018 17:36',
+ rt_departure => '16.10.2018 17:36',
+ sched_arrival => '16.10.2018 18:34',
+ rt_arrival => '16.10.2018 18:34',
+ }
+);
+
+$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->post_ok(
+ '/journey/edit' => form => {
+ action => 'edit',
+ journey_id => 1,
+ }
+);
+
+$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');
+
+$t->post_ok(
+ '/journey/edit' => form => {
+ action => 'save',
+ journey_id => 1,
+ csrf_token => $csrf_token,
+ from_name => 'Münster(Westf)Hbf',
+ to_name => 'Gelsenkirchen Hbf',
+ sched_departure => '16.10.2018 17:36',
+ rt_departure => '16.10.2018 17:42',
+ sched_arrival => '16.10.2018 18:34',
+ rt_arrival => '16.10.2018 18:33',
+ }
+);
+
+$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{..: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{Daten wurden manuell eingetragen});
+
+$t->app->pg->db->query('drop schema travelynx_test_12 cascade');
+done_testing();
diff --git a/t/21-relations.t b/t/21-relations.t
new file mode 100644
index 0000000..857d20d
--- /dev/null
+++ b/t/21-relations.t
@@ -0,0 +1,855 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2023 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_21 cascade');
+$t->app->pg->db->query('create schema travelynx_test_21');
+$t->app->pg->db->query('set search_path to travelynx_test_21');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_21');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1;
+$t->app->start( 'database', 'migrate' );
+
+my $u = $t->app->users;
+
+my $uid1 = $u->add(
+ name => 'test1',
+ email => 'test1@example.org',
+ token => 'abcd',
+ password => q{},
+);
+
+my $uid2 = $u->add(
+ name => 'test2',
+ email => 'test2@example.org',
+ token => 'efgh',
+ password => q{},
+);
+
+$u->verify_registration_token(
+ uid => $uid1,
+ token => 'abcd'
+);
+$u->verify_registration_token(
+ uid => $uid2,
+ token => 'efgh'
+);
+
+$u->set_social(
+ uid => $uid1,
+ accept_follow_requests => 1
+);
+$u->set_social(
+ uid => $uid2,
+ accept_follow_requests => 1
+);
+
+is(
+ $u->get_relation(
+ uid => $uid1,
+ target => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ uid => $uid2,
+ target => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$u->request_follow(
+ uid => $uid1,
+ target => $uid2
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'requests_follow'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 1 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 1
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 1 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 1
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 1 );
+is_deeply(
+ [ $u->get_follow_requests( uid => $uid2 ) ],
+ [ { id => $uid1, name => 'test1' } ]
+);
+is_deeply(
+ [ $u->get_follow_requests( uid => $uid1, sent => 1 ) ],
+ [ { id => $uid2, name => 'test2' } ]
+);
+
+$u->reject_follow_request(
+ uid => $uid2,
+ applicant => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$u->request_follow(
+ uid => $uid1,
+ target => $uid2
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'requests_follow'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 1 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 1
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 1 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 1
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 1 );
+is_deeply(
+ [ $u->get_follow_requests( uid => $uid2 ) ],
+ [ { id => $uid1, name => 'test1' } ]
+);
+is_deeply(
+ [ $u->get_follow_requests( uid => $uid1, sent => 1 ) ],
+ [ { id => $uid2, name => 'test2' } ]
+);
+
+$u->accept_follow_request(
+ uid => $uid2,
+ applicant => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'follows'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 1 );
+is( scalar $u->get_followees( uid => $uid1 ), 1 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+is_deeply(
+ [ $u->get_followers( uid => $uid2 ) ],
+ [
+ {
+ id => $uid1,
+ name => 'test1',
+ following_back => 0,
+ followback_requested => 0,
+ can_follow_back => 0,
+ can_request_follow_back => 1
+ }
+ ]
+);
+is_deeply(
+ [ $u->get_followees( uid => $uid1 ) ],
+ [ { id => $uid2, name => 'test2', following_back => 0 } ]
+);
+
+$u->remove_follower(
+ uid => $uid2,
+ follower => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$u->request_follow(
+ uid => $uid1,
+ target => $uid2
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'requests_follow'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+
+$u->block(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'is_blocked_by'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 1 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+is_deeply(
+ [ $u->get_blocked_users( uid => $uid2 ) ],
+ [ { id => $uid1, name => 'test1' } ]
+);
+
+$u->unblock(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$u->block(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'is_blocked_by'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 1 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+is_deeply(
+ [ $u->get_blocked_users( uid => $uid2 ) ],
+ [ { id => $uid1, name => 'test1' } ]
+);
+
+$u->unblock(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$u->request_follow(
+ uid => $uid1,
+ target => $uid2
+);
+$u->accept_follow_request(
+ uid => $uid2,
+ applicant => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ 'follows'
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 1 );
+is( scalar $u->get_followees( uid => $uid1 ), 1 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+is_deeply(
+ [ $u->get_followers( uid => $uid2 ) ],
+ [
+ {
+ id => $uid1,
+ name => 'test1',
+ following_back => 0,
+ followback_requested => 0,
+ can_follow_back => 0,
+ can_request_follow_back => 1
+ }
+ ]
+);
+is_deeply(
+ [ $u->get_followees( uid => $uid1 ) ],
+ [ { id => $uid2, name => 'test2', following_back => 0 } ]
+);
+
+$u->unfollow(
+ uid => $uid1,
+ target => $uid2
+);
+
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ undef
+);
+is( scalar $u->get_followers( uid => $uid1 ), 0 );
+is( scalar $u->get_followers( uid => $uid2 ), 0 );
+is( scalar $u->get_followees( uid => $uid1 ), 0 );
+is( scalar $u->get_followees( uid => $uid2 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid1 ), 0 );
+is( scalar $u->get_follow_requests( uid => $uid2 ), 0 );
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ scalar $u->get_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( scalar $u->get_blocked_users( uid => $uid1 ), 0 );
+is( scalar $u->get_blocked_users( uid => $uid2 ), 0 );
+is( $u->has_follow_requests( uid => $uid1 ), 0 );
+is( $u->has_follow_requests( uid => $uid2 ), 0 );
+is(
+ $u->has_follow_requests(
+ uid => $uid1,
+ sent => 1
+ ),
+ 0
+);
+is(
+ $u->has_follow_requests(
+ uid => $uid2,
+ sent => 1
+ ),
+ 0
+);
+is( $u->get( uid => $uid1 )->{notifications}, 0 );
+is( $u->get( uid => $uid2 )->{notifications}, 0 );
+
+$t->app->pg->db->query('drop schema travelynx_test_21 cascade');
+done_testing();
diff --git a/t/22-transit-visibility.t b/t/22-transit-visibility.t
new file mode 100644
index 0000000..7e995c5
--- /dev/null
+++ b/t/22-transit-visibility.t
@@ -0,0 +1,487 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2023 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+use DateTime;
+use Travel::Status::DE::IRIS::Result;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_22 cascade');
+$t->app->pg->db->query('create schema travelynx_test_22');
+$t->app->pg->db->query('set search_path to travelynx_test_22');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_22');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1;
+$t->app->start( 'database', 'migrate' );
+
+my $u = $t->app->users;
+
+sub login {
+ my %opt = @_;
+ my $csrf_token
+ = $t->ua->get('/login')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/login' => form => {
+ csrf_token => $csrf_token,
+ user => $opt{user},
+ password => $opt{password},
+ }
+ );
+ $t->status_is(302)->header_is( location => '/' );
+}
+
+sub logout {
+ my $csrf_token
+ = $t->ua->get('/account')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/logout' => form => {
+ csrf_token => $csrf_token,
+ }
+ );
+ $t->status_is(302)->header_is( location => '/login' );
+}
+
+sub test_intransit_visibility {
+ my %opt = @_;
+
+ if ( $opt{set_default_visibility} ) {
+ my %p = %{ $u->get_privacy_by( uid => $opt{uid} ) };
+ $p{default_visibility} = $opt{set_default_visibility};
+ $u->set_privacy(
+ uid => $opt{uid},
+ %p
+ );
+ }
+
+ if ( $opt{set_visibility} ) {
+ $t->app->in_transit->update_visibility(
+ uid => $opt{uid},
+ visibility => $opt{set_visibility}
+ );
+ }
+
+ my $status = $t->app->get_user_status( $opt{uid} );
+ my $token
+ = $status->{sched_departure}->epoch
+ . q{?token=}
+ . $status->{dep_eva} . q{-}
+ . $status->{timestamp}->epoch % 337;
+ my $j_token
+ = $status->{dep_eva} . q{-}
+ . $status->{timestamp}->epoch % 337 . q{-}
+ . $status->{sched_departure}->epoch;
+
+ my $desc
+ = "in_transit vis=$opt{effective_visibility_str} (from $opt{visibility_str})";
+
+ is( $status->{visibility}, $opt{visibility}, $desc );
+ is( $status->{visibility_str}, $opt{visibility_str}, $desc );
+ is( $status->{effective_visibility}, $opt{effective_visibility}, $desc );
+ is( $status->{effective_visibility_str},
+ $opt{effective_visibility_str}, $desc );
+
+ if ( $opt{public} ) {
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{DPN 667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok('/status/test1')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $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)
+ ->content_like(qr{DPN 667});
+ $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{nicht eingecheckt});
+ }
+
+ login(
+ user => 'test1',
+ password => 'password1'
+ );
+
+ # users can see their own status if visibility is >= followrs
+ if ( $opt{effective_visibility} >= 60 ) {
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{DPN 667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok('/status/test1')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $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)
+ ->content_like(qr{DPN 667});
+ $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{nicht eingecheckt});
+ }
+
+ logout();
+ login(
+ user => 'test2',
+ password => 'password2'
+ );
+
+ # uid2 can see uid1 if visibility is >= followers
+ if ( $opt{effective_visibility} >= 60 ) {
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{DPN 667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok('/status/test1')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $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)
+ ->content_like(qr{DPN 667});
+ $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{nicht eingecheckt});
+ }
+
+ logout();
+ login(
+ user => 'test3',
+ password => 'password3'
+ );
+
+ # uid3 can see uid1 if visibility is >= travelynx
+ if ( $opt{effective_visibility} >= 80 ) {
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{DPN 667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok('/status/test1')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $t->get_ok('/ajax/status/test1.html')->status_is(200)
+ ->content_like(qr{nicht eingecheckt});
+ $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)
+ ->content_like(qr{DPN 667});
+ $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{nicht eingecheckt});
+ }
+
+ logout();
+}
+
+my $uid1 = $u->add(
+ name => 'test1',
+ email => 'test1@example.org',
+ token => 'abcd',
+ password => 'password1',
+);
+
+my $uid2 = $u->add(
+ name => 'test2',
+ email => 'test2@example.org',
+ token => 'efgh',
+ password => 'password2',
+);
+
+my $uid3 = $u->add(
+ name => 'test3',
+ email => 'test3@example.org',
+ token => 'ijkl',
+ password => 'password3',
+);
+
+$u->verify_registration_token(
+ uid => $uid1,
+ token => 'abcd'
+);
+$u->verify_registration_token(
+ uid => $uid2,
+ token => 'efgh'
+);
+$u->verify_registration_token(
+ uid => $uid3,
+ token => 'ijkl'
+);
+
+$u->set_social(
+ uid => $uid1,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid2,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid3,
+ accept_follows => 1
+);
+
+$u->follow(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ 'follows'
+);
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+
+my $dep = DateTime->now;
+my $arr = $dep->clone->add( hours => 1 );
+my $train_dep = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-1',
+ departure_ts => '2306251312',
+ platform => 8,
+ station => 'Aachen Hbf',
+ station_uic => 8000001,
+ route_post => 'Mainz Hbf|Aalen Hbf',
+);
+my $train_arr = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-3',
+ arrival_ts => '2306252000',
+ platform => 1,
+ station => 'Aalen Hbf',
+ station_uic => 8000002,
+ route_pre => 'Aachen Hbf|Mainz Hbf',
+);
+$t->app->in_transit->add(
+ uid => $uid1,
+ departure_eva => 8000001,
+ train => $train_dep,
+ route => [],
+);
+$t->app->in_transit->set_arrival_eva(
+ uid => $uid1,
+ arrival_eva => 8000002,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 10,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 10,
+ effective_visibility_str => 'private',
+ public => 0,
+ with_token => 0,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 30,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 60,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 60,
+ effective_visibility_str => 'followers',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 80,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 80,
+ effective_visibility_str => 'travelynx',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 100,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 100,
+ effective_visibility_str => 'public',
+ public => 1,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_visibility => 'private',
+ visibility => 10,
+ visibility_str => 'private',
+ effective_visibility => 10,
+ effective_visibility_str => 'private',
+ public => 0,
+ with_token => 0,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_visibility => 'unlisted',
+ visibility => 30,
+ visibility_str => 'unlisted',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_visibility => 'followers',
+ visibility => 60,
+ visibility_str => 'followers',
+ effective_visibility => 60,
+ effective_visibility_str => 'followers',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_visibility => 'travelynx',
+ visibility => 80,
+ visibility_str => 'travelynx',
+ effective_visibility => 80,
+ effective_visibility_str => 'travelynx',
+ public => 0,
+ with_token => 1,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_visibility => 'public',
+ visibility => 100,
+ visibility_str => 'public',
+ effective_visibility => 100,
+ effective_visibility_str => 'public',
+ public => 1,
+ with_token => 1,
+);
+
+$t->app->in_transit->update_visibility(
+ uid => $uid1,
+ visibility => undef,
+);
+
+test_intransit_visibility(
+ uid => $uid1,
+ set_default_visibility => 10,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 10,
+ effective_visibility_str => 'private',
+ public => 0,
+ with_token => 0,
+);
+
+$t->app->pg->db->query('drop schema travelynx_test_22 cascade');
+done_testing();
diff --git a/t/23-journey-visibility.t b/t/23-journey-visibility.t
new file mode 100644
index 0000000..2124940
--- /dev/null
+++ b/t/23-journey-visibility.t
@@ -0,0 +1,461 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2023 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+use DateTime;
+use Travel::Status::DE::IRIS::Result;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_23 cascade');
+$t->app->pg->db->query('create schema travelynx_test_23');
+$t->app->pg->db->query('set search_path to travelynx_test_23');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_23');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1;
+$t->app->start( 'database', 'migrate' );
+
+my $u = $t->app->users;
+
+sub login {
+ my %opt = @_;
+ my $csrf_token
+ = $t->ua->get('/login')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/login' => form => {
+ csrf_token => $csrf_token,
+ user => $opt{user},
+ password => $opt{password},
+ }
+ );
+ $t->status_is(302)->header_is( location => '/' );
+}
+
+sub logout {
+ my $csrf_token
+ = $t->ua->get('/account')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/logout' => form => {
+ csrf_token => $csrf_token,
+ }
+ );
+ $t->status_is(302)->header_is( location => '/login' );
+}
+
+sub test_journey_visibility {
+ my %opt = @_;
+ my $jid = $opt{journey_id};
+
+ if ( $opt{set_default_visibility} ) {
+ my %p = %{ $u->get_privacy_by( uid => $opt{uid} ) };
+ $p{default_visibility} = $opt{set_default_visibility};
+ $u->set_privacy(
+ uid => $opt{uid},
+ %p
+ );
+ }
+
+ if ( $opt{set_visibility} ) {
+ $t->app->journeys->update_visibility(
+ uid => $opt{uid},
+ id => $jid,
+ visibility => $opt{set_visibility}
+ );
+ }
+
+ my $status = $t->app->get_user_status( $opt{uid} );
+ my $journey = $t->app->journeys->get_single(
+ uid => $opt{uid},
+ journey_id => $jid
+ );
+ my $token
+ = q{?token=}
+ . $status->{dep_eva} . q{-}
+ . $journey->{checkin_ts} % 337 . q{-}
+ . $status->{sched_departure}->epoch;
+
+ my $desc
+ = "journey=$jid vis=$opt{effective_visibility_str} (from $opt{visibility_str})";
+
+ is( $status->{visibility}, $opt{visibility}, $desc );
+ is( $status->{visibility_str}, $opt{visibility_str}, $desc );
+ is( $status->{effective_visibility}, $opt{effective_visibility}, $desc );
+ is( $status->{effective_visibility_str},
+ $opt{effective_visibility_str}, $desc );
+
+ if ( $opt{public} ) {
+ $t->get_ok("/p/test1/j/$jid")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok("/p/test1/j/$jid$token")->status_is(404)
+ ->content_like(qr{Fahrt nicht gefunden.});
+ }
+
+ login(
+ user => 'test1',
+ password => 'password1'
+ );
+
+ # users can see their own status if visibility is >= followrs
+ if ( $opt{effective_visibility} >= 60 ) {
+ $t->get_ok("/p/test1/j/$jid")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok("/p/test1/j/$jid$token")->status_is(404)
+ ->content_like(qr{Fahrt nicht gefunden.});
+ }
+
+ logout();
+ login(
+ user => 'test2',
+ password => 'password2'
+ );
+
+ # uid2 can see uid1 if visibility is >= followers
+ if ( $opt{effective_visibility} >= 60 ) {
+ $t->get_ok("/p/test1/j/$jid")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok("/p/test1/j/$jid$token")->status_is(404)
+ ->content_like(qr{Fahrt nicht gefunden.});
+ }
+
+ logout();
+ login(
+ user => 'test3',
+ password => 'password3'
+ );
+
+ # uid3 can see uid1 if visibility is >= travelynx
+ if ( $opt{effective_visibility} >= 80 ) {
+ $t->get_ok("/p/test1/j/$jid")->status_is(200)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $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)
+ ->content_like(qr{DPN 667});
+ }
+ else {
+ $t->get_ok("/p/test1/j/$jid$token")->status_is(404)
+ ->content_like(qr{Fahrt nicht gefunden.});
+ }
+
+ logout();
+}
+
+my $uid1 = $u->add(
+ name => 'test1',
+ email => 'test1@example.org',
+ token => 'abcd',
+ password => 'password1',
+);
+
+my $uid2 = $u->add(
+ name => 'test2',
+ email => 'test2@example.org',
+ token => 'efgh',
+ password => 'password2',
+);
+
+my $uid3 = $u->add(
+ name => 'test3',
+ email => 'test3@example.org',
+ token => 'ijkl',
+ password => 'password3',
+);
+
+$u->verify_registration_token(
+ uid => $uid1,
+ token => 'abcd'
+);
+$u->verify_registration_token(
+ uid => $uid2,
+ token => 'efgh'
+);
+$u->verify_registration_token(
+ uid => $uid3,
+ token => 'ijkl'
+);
+
+$u->set_social(
+ uid => $uid1,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid2,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid3,
+ accept_follows => 1
+);
+
+$u->follow(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ 'follows'
+);
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+
+my $dep = DateTime->now->subtract( hours => 2 );
+my $arr = DateTime->now->subtract( hours => 1 );
+my $train_dep = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-1',
+ departure_ts => $dep->strftime('%y%m%d%H%M'),
+ platform => 8,
+ station => 'Aachen Hbf',
+ station_uic => 8000001,
+ route_post => 'Mainz Hbf|Aalen Hbf',
+);
+my $train_arr = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-3',
+ arrival_ts => $arr->strftime('%y%m%d%H%M'),
+ platform => 1,
+ station => 'Aalen Hbf',
+ station_uic => 8000002,
+ route_pre => 'Aachen Hbf|Mainz Hbf',
+);
+$t->app->in_transit->add(
+ uid => $uid1,
+ departure_eva => 8000001,
+ train => $train_dep,
+ route => [],
+);
+$t->app->in_transit->set_arrival_eva(
+ uid => $uid1,
+ arrival_eva => 8000002,
+);
+
+my $db = $t->app->pg->db;
+my $tx = $db->begin;
+
+my $journey = $t->app->in_transit->get(
+ uid => $uid1,
+ db => $db,
+);
+my $jid = $t->app->journeys->add_from_in_transit(
+ journey => $journey,
+ db => $db
+);
+$t->app->in_transit->delete(
+ uid => $uid1,
+ db => $db
+);
+$tx->commit;
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_default_visibility => 10,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 10,
+ effective_visibility_str => 'private',
+ public => 0,
+ with_token => 0,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_default_visibility => 30,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_default_visibility => 60,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 60,
+ effective_visibility_str => 'followers',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_default_visibility => 80,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 80,
+ effective_visibility_str => 'travelynx',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_default_visibility => 100,
+ visibility => undef,
+ visibility_str => 'default',
+ effective_visibility => 100,
+ effective_visibility_str => 'public',
+ public => 1,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'private',
+ visibility => 10,
+ visibility_str => 'private',
+ effective_visibility => 10,
+ effective_visibility_str => 'private',
+ public => 0,
+ with_token => 0,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'unlisted',
+ visibility => 30,
+ visibility_str => 'unlisted',
+ effective_visibility => 30,
+ effective_visibility_str => 'unlisted',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'followers',
+ visibility => 60,
+ visibility_str => 'followers',
+ effective_visibility => 60,
+ effective_visibility_str => 'followers',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'travelynx',
+ visibility => 80,
+ visibility_str => 'travelynx',
+ effective_visibility => 80,
+ effective_visibility_str => 'travelynx',
+ public => 0,
+ with_token => 1,
+);
+
+test_journey_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'public',
+ visibility => 100,
+ visibility_str => 'public',
+ effective_visibility => 100,
+ effective_visibility_str => 'public',
+ public => 1,
+ with_token => 1,
+);
+
+$t->app->pg->db->query('drop schema travelynx_test_23 cascade');
+done_testing();
diff --git a/t/24-past-visibility.t b/t/24-past-visibility.t
new file mode 100644
index 0000000..51c8081
--- /dev/null
+++ b/t/24-past-visibility.t
@@ -0,0 +1,558 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2023 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Tests journey entry and statistics
+
+use Test::More;
+use Test::Mojo;
+
+use DateTime;
+use Travel::Status::DE::IRIS::Result;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query('drop schema if exists travelynx_test_24 cascade');
+$t->app->pg->db->query('create schema travelynx_test_24');
+$t->app->pg->db->query('set search_path to travelynx_test_24');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_test_24');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1;
+$t->app->start( 'database', 'migrate' );
+
+my $u = $t->app->users;
+
+sub login {
+ my %opt = @_;
+ my $csrf_token
+ = $t->ua->get('/login')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/login' => form => {
+ csrf_token => $csrf_token,
+ user => $opt{user},
+ password => $opt{password},
+ }
+ );
+ $t->status_is(302)->header_is( location => '/' );
+}
+
+sub logout {
+ my $csrf_token
+ = $t->ua->get('/account')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+ $t->post_ok(
+ '/logout' => form => {
+ csrf_token => $csrf_token,
+ }
+ );
+ $t->status_is(302)->header_is( location => '/login' );
+}
+
+sub test_history_visibility {
+ my %opt = @_;
+ my $jid = $opt{journey_id};
+
+ if ( $opt{set_default_visibility} ) {
+ my %p = %{ $u->get_privacy_by( uid => $opt{uid} ) };
+ $p{default_visibility} = $opt{set_default_visibility};
+ $u->set_privacy(
+ uid => $opt{uid},
+ %p
+ );
+ }
+
+ if ( $opt{set_past_visibility} ) {
+ my %p = %{ $u->get_privacy_by( uid => $opt{uid} ) };
+ $p{past_visibility} = $opt{set_past_visibility};
+ $u->set_privacy(
+ uid => $opt{uid},
+ %p
+ );
+ }
+
+ if ( $opt{set_visibility} ) {
+ $t->app->journeys->update_visibility(
+ uid => $opt{uid},
+ id => $jid,
+ visibility => $opt{set_visibility}
+ );
+ }
+
+ my $status = $t->app->get_user_status( $opt{uid} );
+ my $journey = $t->app->journeys->get_single(
+ uid => $opt{uid},
+ journey_id => $jid,
+ with_visibility => 1,
+ );
+ my $token
+ = q{?token=}
+ . $status->{dep_eva} . q{-}
+ . $journey->{checkin_ts} % 337 . q{-}
+ . $status->{sched_departure}->epoch;
+
+ $opt{set_past_visibility} //= q{};
+ my $desc
+ = "history vis=$opt{set_past_visibility} journey=$jid vis=$journey->{effective_visibility_str}";
+
+ if ( $opt{public} ) {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_like( qr{DPN 667}, "public $desc" );
+ }
+ else {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_unlike( qr{DPN 667}, "public $desc" );
+ }
+
+ login(
+ user => 'test1',
+ password => 'password1'
+ );
+
+ if ( $opt{self} ) {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_like( qr{DPN 667}, "self $desc" );
+ }
+ else {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_unlike( qr{DPN 667}, "self $desc" );
+ }
+
+ logout();
+ login(
+ user => 'test2',
+ password => 'password2'
+ );
+
+ if ( $opt{followers} ) {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_like( qr{DPN 667}, "follower $desc" );
+ }
+ else {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_unlike( qr{DPN 667}, "follower $desc" );
+ }
+
+ logout();
+ login(
+ user => 'test3',
+ password => 'password3'
+ );
+
+ if ( $opt{travelynx} ) {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_like( qr{DPN 667}, "travelynx $desc" );
+ }
+ else {
+ $t->get_ok('/p/test1')->status_is(200)
+ ->content_unlike( qr{DPN 667}, "travelynx $desc" );
+ }
+
+ logout();
+}
+
+my $uid1 = $u->add(
+ name => 'test1',
+ email => 'test1@example.org',
+ token => 'abcd',
+ password => 'password1',
+);
+
+my $uid2 = $u->add(
+ name => 'test2',
+ email => 'test2@example.org',
+ token => 'efgh',
+ password => 'password2',
+);
+
+my $uid3 = $u->add(
+ name => 'test3',
+ email => 'test3@example.org',
+ token => 'ijkl',
+ password => 'password3',
+);
+
+$u->verify_registration_token(
+ uid => $uid1,
+ token => 'abcd'
+);
+$u->verify_registration_token(
+ uid => $uid2,
+ token => 'efgh'
+);
+$u->verify_registration_token(
+ uid => $uid3,
+ token => 'ijkl'
+);
+
+$u->set_social(
+ uid => $uid1,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid2,
+ accept_follows => 1
+);
+$u->set_social(
+ uid => $uid3,
+ accept_follows => 1
+);
+
+$u->follow(
+ uid => $uid2,
+ target => $uid1
+);
+
+is(
+ $u->get_relation(
+ subject => $uid2,
+ object => $uid1
+ ),
+ 'follows'
+);
+is(
+ $u->get_relation(
+ subject => $uid1,
+ object => $uid2
+ ),
+ undef
+);
+
+my $dep = DateTime->now->subtract( hours => 2 );
+my $arr = DateTime->now->subtract( hours => 1 );
+my $train_dep = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-1',
+ departure_ts => $dep->strftime('%y%m%d%H%M'),
+ platform => 8,
+ station => 'Aachen Hbf',
+ station_uic => 8000001,
+ route_post => 'Mainz Hbf|Aalen Hbf',
+);
+my $train_arr = Travel::Status::DE::IRIS::Result->new(
+ classes => 'N',
+ type => 'DPN',
+ train_no => '667',
+ raw_id => '1234-2306251312-3',
+ arrival_ts => $arr->strftime('%y%m%d%H%M'),
+ platform => 1,
+ station => 'Aalen Hbf',
+ station_uic => 8000002,
+ route_pre => 'Aachen Hbf|Mainz Hbf',
+);
+$t->app->in_transit->add(
+ uid => $uid1,
+ departure_eva => 8000001,
+ train => $train_dep,
+ route => [],
+);
+$t->app->in_transit->set_arrival_eva(
+ uid => $uid1,
+ arrival_eva => 8000002,
+);
+
+$t->app->in_transit->update_visibility(
+ uid => $uid1,
+ visibility => 'public',
+);
+
+my $db = $t->app->pg->db;
+my $tx = $db->begin;
+
+my $journey = $t->app->in_transit->get(
+ uid => $uid1,
+ db => $db,
+);
+my $jid = $t->app->journeys->add_from_in_transit(
+ journey => $journey,
+ db => $db
+);
+$t->app->in_transit->delete(
+ uid => $uid1,
+ db => $db
+);
+$tx->commit;
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_past_visibility => 60,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_past_visibility => 80,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_past_visibility => 100,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 1,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'private',
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'private',
+ set_past_visibility => 60,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'private',
+ set_past_visibility => 80,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'private',
+ set_past_visibility => 100,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'unlisted',
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'unlisted',
+ set_past_visibility => 60,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'unlisted',
+ set_past_visibility => 80,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'unlisted',
+ set_past_visibility => 100,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'followers',
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'followers',
+ set_past_visibility => 60,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'followers',
+ set_past_visibility => 80,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'followers',
+ set_past_visibility => 100,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'travelynx',
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'travelynx',
+ set_past_visibility => 60,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'travelynx',
+ set_past_visibility => 80,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'travelynx',
+ set_past_visibility => 100,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'public',
+ set_past_visibility => 10,
+ self => 0,
+ followers => 0,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'public',
+ set_past_visibility => 60,
+ self => 1,
+ followers => 1,
+ travelynx => 0,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'public',
+ set_past_visibility => 80,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 0,
+);
+
+test_history_visibility(
+ uid => $uid1,
+ journey_id => $jid,
+ set_visibility => 'public',
+ set_past_visibility => 100,
+ self => 1,
+ followers => 1,
+ travelynx => 1,
+ public => 1,
+);
+
+$t->app->pg->db->query('drop schema travelynx_test_24 cascade');
+done_testing();
diff --git a/t/r-negative-delay.t b/t/r-negative-delay.t
new file mode 100644
index 0000000..78bd6e0
--- /dev/null
+++ b/t/r-negative-delay.t
@@ -0,0 +1,101 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
+#
+# SPDX-License-Identifier: MIT
+
+use Mojo::Base -strict;
+
+# Regression test: handle negative cumulative arrival / departure delay
+
+use Test::More;
+use Test::Mojo;
+
+# Include application
+use FindBin;
+require "$FindBin::Bin/../index.pl";
+
+my $t = Test::Mojo->new('Travelynx');
+
+if ( not $t->app->config->{db} ) {
+ plan( skip_all => 'No database configured' );
+}
+
+$t->app->pg->db->query(
+ 'drop schema if exists travelynx_regr_negative_delay cascade');
+$t->app->pg->db->query('create schema travelynx_regr_negative_delay');
+$t->app->pg->db->query('set search_path to travelynx_regr_negative_delay');
+$t->app->pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do('set search_path to travelynx_regr_negative_delay');
+ }
+);
+
+$t->app->config->{mail}->{disabled} = 1;
+
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
+$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 => '/' );
+
+$csrf_token
+ = $t->ua->get('/journey/add')->res->dom->at('input[name=csrf_token]')
+ ->attr('value');
+$t->post_ok(
+ '/journey/add' => form => {
+ csrf_token => $csrf_token,
+ action => 'save',
+ train => 'RE 42 11238',
+ dep_station => 'EMST',
+ sched_departure => '16.10.2018 17:36',
+ rt_departure => '16.10.2018 17:35',
+ arr_station => 'EG',
+ sched_arrival => '16.10.2018 18:34',
+ rt_arrival => '16.10.2018 18:32',
+ }
+);
+$t->status_is(302)->header_is( location => '/journey/1' );
+
+$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});
+
+$t->app->pg->db->query('drop schema travelynx_regr_negative_delay cascade');
+done_testing();
diff --git a/templates/_cancelled.html.ep b/templates/_cancelled.html.ep
deleted file mode 100644
index be3e318..0000000
--- a/templates/_cancelled.html.ep
+++ /dev/null
@@ -1,27 +0,0 @@
-<div class="card info-color">
- <div class="card-content">
- <span class="card-title">Zugausfall dokumentieren</span>
- <p>Prinzipiell wärest du nun eingecheckt in
- <%= $journey->{train_type} %> <%= $journey->{train_no} %>
- ab <%= $journey->{dep_name} %>, doch dieser Zug fällt aus.
- </p>
- <p>Falls du den Zugausfall z.B. für ein Fahrgastrechteformular
- dokumentieren möchtest, wähle bitte jetzt deine geplante
- Zielstation aus. Achtung: Momentan wird dabei keine
- Soll-Ankunftszeit gespeichert, das zu beheben steht auf
- der Todoliste.</p>
- <table>
- <tbody>
- % my $is_after = 0;
- % for my $station (@{$journey->{route_after}}) {
- <tr><td><a class="action-cancelled-to" data-station="<%= $station->[0] %>"><%= $station->[0] %></a></td></tr>
- % }
- </tbody>
- </table>
- </div>
- <div class="card-action">
- <a class="action-undo" data-id="in_transit">
- <i class="material-icons">undo</i> Checkinversuch Rückgängig?
- </a>
- </div>
-</div>
diff --git a/templates/_cancelled_departure.html.ep b/templates/_cancelled_departure.html.ep
new file mode 100644
index 0000000..79492a5
--- /dev/null
+++ b/templates/_cancelled_departure.html.ep
@@ -0,0 +1,13 @@
+<div class="card">
+ <div class="card-content">
+ <span class="card-title">Zugausfall</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.
+ </p>
+ % if (my @connections = @{stash('connections_iris') // []}) {
+ <p>Alternative Reisemöglichkeiten:</p>
+ %= include '_connections', connections => \@connections, checkin_from => $journey->{dep_eva};
+ % }
+ </div>
+</div>
diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep
index 0d71c93..7155208 100644
--- a/templates/_checked_in.html.ep
+++ b/templates/_checked_in.html.ep
@@ -1,8 +1,14 @@
+% my $user = current_user();
<div class="autorefresh">
<div class="card">
<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">Eingecheckt in <%= $journey->{train_type} %> <%= $journey->{train_no} %></span>
+ <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">
+ %= include '_format_train', journey => $journey
+ </span>
% if ($journey->{comment}) {
<p><%= $journey->{comment} %></p>
% }
@@ -12,28 +18,21 @@
data-route="<%= journey_to_ajax_route($journey) %>"
data-dest="<%= $journey->{arr_name} %>"
>
- % if ($journey->{boarding_countdown} > 120) {
- Einfahrt in <%= sprintf('%.f', $journey->{boarding_countdown} / 60) %> Minuten<br/>
- % }
- % elsif ($journey->{boarding_countdown} > 60) {
- Einfahrt in einer Minute<br/>
+ % if ($journey->{boarding_countdown} > 60) {
+ Einfahrt in <%= journeys->min_to_human(int($journey->{boarding_countdown} / 60)) %><br/>
% }
% elsif ($journey->{boarding_countdown} > 0) {
- Zug fährt ein<br/>
- % }
- % if ($journey->{departure_countdown} > 120) {
- Abfahrt in <%= sprintf('%.f', $journey->{departure_countdown} / 60) %> Minuten
+ Fährt ein<br/>
% }
- % elsif ($journey->{departure_countdown} > 60) {
- Abfahrt in einer Minute
+ % if ($journey->{departure_countdown} > 60) {
+ Abfahrt in <%= journeys->min_to_human(int($journey->{departure_countdown} / 60)) %>
% }
% elsif ($journey->{departure_countdown} > 0) {
Abfahrt in weniger als einer Minute
% }
% elsif (defined $journey->{arrival_countdown}) {
% if ($journey->{arrival_countdown} > 60) {
- Ankunft in <%= sprintf('%.f', $journey->{arrival_countdown} / 60) %>
- Minute<%= sprintf('%.f', $journey->{arrival_countdown} / 60) == 1 ? '' : 'n' %>
+ Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %>
% }
% elsif ($journey->{arrival_countdown} > 0) {
Ankunft in weniger als einer Minute
@@ -77,12 +76,22 @@
% @wagons = reverse @wagons;
% }
% }
- <a href="https://marudor.de/details/<%= $journey->{train_type} %>%20<%= $journey->{train_no} %>/<%= DateTime->now(time_zone => 'Europe/Berlin')->epoch %>000">
+ <a href="https://dbf.finalrewind.org/_wr/<%= $journey->{train_no} %>/<%= $journey->{sched_departure}->strftime('%Y%m%d%H%M') %>?e=<%= $journey->{dep_direction} // q{} %>">
%= $direction
+ % my $gi;
% for my $wagon (@wagons) {
% if (not ($wagon->is_locomotive or $wagon->is_powercar)) {
- %= $wagon->number || $wagon->type
+ % if (defined $gi and $gi != $wagon->group_index) {
+ •
+ % }
+ % if ($wagon->is_closed) {
+ X
+ % }
+ % else {
+ %= $wagon->number || ($wagon->type =~ m{AB} ? '½' : $wagon->type =~ m{A} ? '1.' : $wagon->type =~ m{B} ? '2.' : $wagon->type )
+ % }
% }
+ % $gi = $wagon->group_index;
% }
%= $direction
</a>
@@ -102,7 +111,12 @@
% }
</div>
<div style="float: right; text-align: right;">
- <b><%= $journey->{arr_name} %></b><br/>
+ % if ($user->{sb_template}) {
+ <b><a href="<%= resolve_sb_template($user->{sb_template}, name => $journey->{arr_name}, eva => $journey->{arr_eva}, tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id}) %>" class="unmarked"><%= $journey->{arr_name} %></a></b><br/>
+ % }
+ % else {
+ <b><%= $journey->{arr_name} %></b><br/>
+ % }
% if ($journey->{real_arrival}->epoch) {
<b><%= $journey->{real_arrival}->strftime('%H:%M') %></b>
% if ($journey->{real_arrival}->epoch != $journey->{sched_arrival}->epoch) {
@@ -118,19 +132,21 @@
% if ($station->[0] eq $journey->{arr_name}) {
% last;
% }
- % if (($station->[1]{rt_arr_countdown} // 0) > 0) {
- <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %>
- % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60);
+ % if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
+ <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ % if ($station->[2]{arr_delay}) {
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
% }
% last;
% }
- % if (($station->[1]{rt_dep_countdown} // 0) > 0) {
+ % if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{dep}) {
<%= $station->[0] %><br/>
- <%= $station->[1]{rt_arr}->strftime('%H:%M') %> →
- <%= $station->[1]{rt_dep}->strftime('%H:%M') %>
- % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60);
+ % if ($station->[2]{arr}) {
+ <%= $station->[2]{arr}->strftime('%H:%M') %> →
+ % }
+ %= $station->[2]{dep}->strftime('%H:%M')
+ % if ($station->[2]{dep_delay}) {
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
% }
% last;
% }
@@ -143,19 +159,19 @@
% if ($station->[0] eq $journey->{arr_name}) {
% last;
% }
- % if (($station->[1]{rt_arr_countdown} // 0) > 0) {
- <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %>
- % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60);
+ % if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
+ <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ % if ($station->[2]{arr_delay}) {
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
% }
% last;
% }
- % if (($station->[1]{rt_dep_countdown} // 0) > 0) {
+ % if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{arr} and $station->[2]{dep}) {
<%= $station->[0] %><br/>
- <%= $station->[1]{rt_arr}->strftime('%H:%M') %> →
- <%= $station->[1]{rt_dep}->strftime('%H:%M') %>
- % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60);
+ <%= $station->[2]{arr}->strftime('%H:%M') %> →
+ <%= $station->[2]{dep}->strftime('%H:%M') %>
+ % if ($station->[2]{dep_delay}) {
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
% }
% last;
% }
@@ -163,9 +179,20 @@
</div>
</p>
% }
- % if (@{$journey->{messages} // []} or @{$journey->{extra_data}{him_msg} // []} or @{$journey->{extra_data}{qos_msg} // []}) {
+ % if ($journey->{extra_data}{cancelled_destination}) {
+ <p style="margin-bottom: 2ex;">
+ Der Halt an der Zielstation <b><%=
+ $journey->{extra_data}{cancelled_destination} %></b> entfällt.
+ Die zugehörige Fahrt wurde bereits als ausgefallen eingetragen.
+ Bitte wähle ein neues Reiseziel.
+ </p>
+ % }
+ % 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
+ % }
% for my $message (reverse @{$journey->{messages} // []}) {
% if ($journey->{sched_departure}->epoch - $message->[0]->epoch < 1800) {
<li> <i class="material-icons tiny">warning</i> <%= $message->[0]->strftime('%H:%M') %>: <%= $message->[1] %></li>
@@ -176,57 +203,67 @@
<li> <i class="material-icons tiny">info</i> <%= $message->[0]->strftime('%H:%M') %>: <%= $message->[1] %></li>
% }
% }
- % for my $message (@{$journey->{extra_data}{him_msg} // []}) {
- <li> <i class="material-icons tiny">info</i> <%= $message->{header} %> <%= $message->{lead} %></li>
- % }
</ul>
</p>
% }
- % if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} < (20*60)) {
- % if (my @connections = get_connecting_trains()) {
- <span class="card-title" style="margin-top: 2ex;">Verbindungen</span>
- % if ($journey->{arrival_countdown} < 0) {
- <p>Zug auswählen zum Einchecken mit Zielwahl.</p>
- % }
- %= include '_connections', connections => \@connections, checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_ds100} : undef;
+ % if (@{stash('connections_iris') // [] } or @{stash('connections_hafas') // []}) {
+ <span class="card-title" style="margin-top: 2ex;">Verbindungen</span>
+ % if ($journey->{arrival_countdown} < 0) {
+ <p>Fahrt auswählen zum Einchecken mit Zielwahl.</p>
+ % }
+ % if (@{stash('connections_iris') // [] }) {
+ %= include '_connections', connections => stash('connections_iris'), checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_eva} : undef;
+ % }
+ % if (@{stash('connections_hafas') // [] }) {
+ %= include '_connections_hafas', connections => stash('connections_hafas'), checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_eva} : undef;
% }
% }
% if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) {
<p style="margin-top: 2ex;">
- Der automatische Checkout erfolgt wegen gelegentlich veralteter
- IRIS-Daten erst etwa zehn Minuten nach der Ankunft.
+ Der automatische Checkout erfolgt wegen teilweise langsamer
+ Echtzeitdatenupdates erst etwa zehn Minuten nach der Ankunft.
</p>
% }
% elsif (not $journey->{arr_name}) {
- <p>Ziel wählen:</p>
- <table>
- <tbody>
- % for my $station (@{$journey->{route_after}}) {
- <tr><td><a class="action-checkout" data-station="<%= $station->[0] %>"><%= $station->[0] %>
- % if ($station->[2] and $station->[2] eq 'cancelled') {
- <span style="float: right;">entfällt</span>
- % }
- % elsif ($station->[1]{rt_arr}) {
- <span style="float: right;"><%= $station->[1]{rt_arr}->strftime('%H:%M') %></span>
- % }
- % elsif ($station->[2] and $station->[2] eq 'additional') {
- <span style="float: right;">Zusatzhalt</span>
- % }
- </a></td></tr>
- % }
- </tbody>
- </table>
+ <p>
+ % for my $station (@{$journey->{route_after}}) {
+ <a class="tablerow action-checkout" data-station="<%= $station->[1] // $station->[0] %>">
+ <span><%= $station->[0] %></span>
+ <span>
+ % if ($station->[2]{load}{SECOND}) {
+ % my ($first, $second) = load_icon($station->[2]{load});
+ <i class="material-icons tiny" aria-hidden="true"><%= $first %></i> <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
+ % }
+ % 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>
+ % }
+ </p>
% }
</div>
<div class="card-action">
% if ($journey->{arr_name}) {
- <a style="margin-right: 0;" href="/journey/comment">
- <i class="material-icons left">comment</i> Kommentar
+ <a href="/journey/comment">
+ <i class="material-icons">comment</i>
+ </a>
+ <a style="margin-right: 0;" href="/journey/visibility">
+ <i class="material-icons"><%= visibility_icon($journey_visibility) %></i>
</a>
% }
% 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">undo</i> Checkin Rückgängig
+ <i class="material-icons left" aria-hidden="true">undo</i> Rückgängig
</a>
% }
% if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) {
@@ -235,67 +272,146 @@
style="margin-right: 0;"
data-station="<%= $journey->{arr_name}%>">
<i class="material-icons left">done</i>
- Jetzt auschecken
+ Auschecken
</a>
% }
% elsif ($journey->{arr_name}) {
- % my $attrib = 'im';
+ % my $attrib = 'in';
% if ($journey->{train_type} =~ m{ ^ (?: S | RB ) $ }x) {
% $attrib = 'in der';
% }
<a class="action-share blue-text right"
style="margin-right: 0;"
- % if (current_user()->{is_public} & 0x04 and $journey->{comment}) {
- data-text="<%= $journey->{comment} %> (@ <%= $journey->{train_type} %> <%= $journey->{train_no} %> → <%= $journey->{arr_name} %>) #NowTräwelling #dbl"
+ % my $arr_text = q{};
+ % if ($journey->{real_arrival}->epoch and $journey_visibility eq 'private') {
+ % $arr_text = $journey->{real_arrival}->strftime(' – Ankunft gegen %H:%M Uhr');
+ % }
+ % if ($user->{comments_visible} and $journey->{comment}) {
+ data-text="<%= $journey->{comment} %> (@ <%= $journey->{train_type} %> <%= $journey->{train_no} %> → <%= $journey->{arr_name} %>) #travelynx"
% }
% else {
- data-text="Ich bin gerade <%= $attrib %> <%= $journey->{train_type} %> <%= $journey->{train_no} %> nach <%= $journey->{arr_name} %> #NowTräwelling #dbl"
+ data-text="Ich bin gerade <%= $attrib %> <%= $journey->{train_type} %> <%= $journey->{train_no} %> nach <%= $journey->{arr_name} . $arr_text %> #travelynx"
+ % }
+ % if ($journey_visibility eq 'public') {
+ data-url="<%= url_for('/status')->to_abs->scheme('https') %>/<%= $user->{name} %>/<%= $journey->{sched_departure}->epoch %>"
% }
- % if (current_user()->{is_public} & 0x02) {
- data-url="<%= url_for('/status')->to_abs->scheme('https') %>/<%= current_user->{name} %>/<%= $journey->{sched_departure}->epoch %>"
+ % elsif ($journey_visibility eq 'travelynx' or $journey_visibility eq 'followers' or $journey_visibility eq 'unlisted') {
+ 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">share</i> Teilen
+ <i class="material-icons left" aria-hidden="true">share</i> Teilen
+ </a>
+ % }
+ % else {
+ <a class="right" href="/journey/visibility">
+ <i class="material-icons left"><%= visibility_icon($journey_visibility) %></i> Sichtbarkeit
</a>
% }
</div>
</div>
+ % if (@{stash('timeline') // []}) {
+ %= include '_timeline_link', timeline => stash('timeline'), from_checkin => 1
+ % }
% if ($journey->{arr_name}) {
+ <div class="card" style="margin-top: <%= scalar @{stash('timeline') // []} ? '1.5rem' : '3em' %>;">
+ <div class="card-content">
+ <span class="card-title">Details</span>
+ % if (@{$journey->{extra_data}{him_msg} // []}) {
+ <p style="margin-bottom: 2ex;">
+ <ul>
+ % for my $message (@{$journey->{extra_data}{him_msg} // []}) {
+ <li> <i class="material-icons tiny">info</i> <%= $message->{header} %> <%= $message->{lead} %></li>
+ % }
+ </ul>
+ </p>
+ % }
+ % if ($journey->{traewelling}{errored} and $journey->{traewelling_log_latest}) {
+ <p style="margin-bottom: 2ex;">
+ <ul>
+ <li> <i class="material-icons tiny">warning</i> Träwelling: <%= $journey->{traewelling_log_latest} %></li>
+ </ul>
+ </p>
+ % }
+ % if ($journey->{traewelling_url}) {
+ <p style="margin-bottom: 2ex;">
+ <ul>
+ <li> <i class="material-icons tiny">sync</i> Träwelling: <a href="<%= $journey->{traewelling_url} %>"><%= $journey->{traewelling_log_latest} %></a></li>
+ </ul>
+ </p>
+ % }
+ </div>
+ <div class="card-action">
+ % my $url = 'https://bahn.expert/details/';
+ % if ($journey->{train_id} =~ m{[|]}) {
+ % $url = $url . '/' . $journey->{sched_departure}->epoch . '000?jid=' . $journey->{train_id};
+ % }
+ % else {
+ % $url = $url . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . $journey->{sched_departure}->epoch . '000?station=' . $journey->{dep_eva};
+ % }
+ <a style="margin-right: 0;" href="<%= $url %>"><i class="material-icons left" aria-hidden="true">timeline</i> Zuglauf</a>
+ % if ($journey->{extra_data}{trip_id}) {
+ <a class="right" style="margin-right: 0;" href="https://dbf.finalrewind.org/map/<%= $journey->{extra_data}{trip_id} %>/<%= $journey->{train_line} || 0 %>?from=<%= $journey->{dep_name} %>&amp;to=<%= $journey->{arr_name} %>&amp;dark=<%= (session('theme') and session('theme') eq 'dark') ? 1 : 0 %>"><i class="material-icons left" aria-hidden="true">map</i> Karte</a>
+ % }
+ </div>
+ </div>
<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>
- <table>
- <tbody>
- % for my $station (@{$journey->{route_after}}) {
- % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name});
- <tr><td><a style="<%= $is_dest? 'font-weight: bold;' : '' %>" class="action-checkout" data-station="<%= $station->[0] %>"><%= $station->[0] %>
- % if ($station->[2] and $station->[2] eq 'cancelled') {
- <span style="float: right;">entfällt</span>
- % }
- % elsif ($station->[1]{rt_arr}) {
- <span style="float: right;"><%= $station->[1]{rt_arr}->strftime('%H:%M') %></span>
- % }
- % elsif ($station->[2] and $station->[2] eq 'additional') {
- <span style="float: right;">Zusatzhalt</span>
- % }
- </a></td></tr>
+ % if ($user->{sb_template}) {
+ <div class="targetlist">
+ % }
+ % else {
+ <p>
+ % }
+ % for my $station (@{$journey->{route_after}}) {
+ % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name});
+ <a class="action-checkout tablerow" style="<%= $is_dest? 'font-weight: bold;' : '' %>" data-station="<%= $station->[1] // $station->[0] %>">
+ <span><%= $station->[0] %></span>
+ <span>
+ % if ($station->[2]{load}{SECOND}) {
+ % my ($first, $second) = load_icon($station->[2]{load});
+ <i class="material-icons tiny" aria-hidden="true"><%= $first %></i> <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
% }
- </tbody>
- </table>
- <p>
- Falls das Backend ausgefallen ist oder der Zug aus anderen
- Gründen verloren ging: <a class="action-checkout"
- data-force="1" data-station="<%= $journey->{arr_name}
- %>">Ohne Echtzeitdaten in <%= $journey->{arr_name} %>
- auschecken</a>.
- </p>
+ % 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>
+ % if ($user->{sb_template}) {
+ <a class="nonflex" href="<%= resolve_sb_template($user->{sb_template}, name => $station->[0], eva => $station->[1], tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id}) %>"><i class="material-icons tiny">train</i></a>
+ % }
+ % }
+ % if ($user->{sb_template}) {
+ </div>
+ % }
+ % else {
+ </p>
+ % }
</div>
<div class="card-action">
<a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;">
- <i class="material-icons left">undo</i> Checkin Rückgängig
+ <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig
</a>
</div>
</div>
+ <p>
+ Falls das Backend ausgefallen ist oder die Fahrt aus anderen
+ Gründen verloren ging:
+ </p>
+ <p class="center-align">
+ <a class="action-checkout waves-light btn"
+ data-force="1" data-station="<%= $journey->{arr_name}
+ %>">Ohne Echtzeitdaten auschecken</a>
+ </p>
% }
</div>
diff --git a/templates/_checked_out.html.ep b/templates/_checked_out.html.ep
index ca9373d..5a944dc 100644
--- a/templates/_checked_out.html.ep
+++ b/templates/_checked_out.html.ep
@@ -1,13 +1,17 @@
<div class="card">
<div class="card-content">
<span class="card-title">Ausgecheckt</span>
- <p>Aus <%= $journey->{train_type} %> <%= $journey->{train_no} %>
- bis <a href="/s/<%= $journey->{arr_ds100} %>"><%= $journey->{arr_name} %></a></p>
- % if (now()->epoch - $journey->{timestamp}->epoch < (30*60)) {
- % if (my @connections = get_connecting_trains()) {
- <span class="card-title" style="margin-top: 2ex;">Verbindungen</span>
- <p>Zug auswählen zum Einchecken mit Zielwahl.</p>
- %= include '_connections', connections => \@connections, checkin_from => $journey->{arr_ds100};
+ <p>Aus
+ %= include '_format_train', journey => $journey
+ bis <a href="/s/<%= $journey->{arr_eva} %>?hafas=<%= $journey->{train_id} =~ m{[|]} ? 1 : 0 %>"><%= $journey->{arr_name} %></a></p>
+ % if (@{stash('connections_iris') // [] } or @{stash('connections_hafas') // []}) {
+ <span class="card-title" style="margin-top: 2ex;">Verbindungen</span>
+ <p>Fahrt auswählen zum Einchecken mit Zielwahl.</p>
+ % if (@{stash('connections_iris') // [] }) {
+ %= include '_connections', connections => stash('connections_iris'), checkin_from => $journey->{arr_eva};
+ % }
+ % if (@{stash('connections_hafas') // [] }) {
+ %= include '_connections_hafas', connections => stash('connections_hafas'), checkin_from => $journey->{arr_eva};
% }
% }
</div>
diff --git a/templates/_connections.html.ep b/templates/_connections.html.ep
index 2837e93..1dd2718 100644
--- a/templates/_connections.html.ep
+++ b/templates/_connections.html.ep
@@ -1,89 +1,76 @@
-<div class="hide-on-med-and-up"><table class="striped"><tbody>
+<ul class="collection departures connections">
% for my $res (@{$connections}) {
- % my ($train, $via) = @{$res};
- % my $td_class = '';
+ % my ($train, $via, $via_arr, $load) = @{$res};
+ % $via_arr = $via_arr ? $via_arr->strftime('%H:%M') : q{};
+ % my $row_class = '';
% my $link_class = 'action-checkin';
% if ($train->is_cancelled) {
- % $td_class = 'cancelled';
+ % $row_class = 'cancelled';
% $link_class = 'action-cancelled-from';
% }
- <tr>
- <td class="<%= $td_class %>">
- % if ($checkin_from) {
- <a class="<%= $link_class %>" data-station="<%= $train->station_uic %>" data-train="<%= $train->train_id %>" data-dest="<%= $via %>"><%= $train->line %></a>
- % }
- % else {
- %= $train->line
- % }
- </td>
- <td class="<%= $td_class %>">
- % if ($checkin_from) {
- <a class="<%= $link_class %>" data-station="<%= $train->station_uic %>" data-train="<%= $train->train_id %>" data-dest="<%= $via %>"><%= $via %></a>
- % }
- % else {
- %= $via
- % }
- </td>
- <td>
+ % if ($checkin_from) {
+ <li class="collection-item <%= $row_class %> <%= $link_class %>"
+ data-station="<%= $train->station_uic %>"
+ data-train="<%= $train->train_id %>"
+ data-ts="<%= ($train->sched_departure // $train->departure)->epoch %>"
+ data-dest="<%= $via->{name} %>">
+ % }
+ % else {
+ <li class="collection-item <%= $row_class %>">
+ % }
+ <a class="dep-time" href="#">
% if ($train->departure_is_cancelled) {
%= $train->sched_departure->strftime('%H:%M')
- ⊖
% }
% else {
%= $train->departure->strftime('%H:%M')
- % if ($train->departure_delay) {
- %= sprintf('(%+d)', $train->departure_delay)
- % }
- <br/>Gleis <%= $train->platform || '?' %>
% }
- </td>
- </tr>
- % }
-</tbody></table></div>
-<div class="hide-on-small-only"><table class="striped"><tbody>
- % for my $res (@{$connections}) {
- % my ($train, $via) = @{$res};
- % my $td_class = '';
- % my $link_class = 'action-checkin';
- % if ($train->departure_is_cancelled) {
- % $td_class = 'cancelled';
- % $link_class = 'action-cancelled-from';
- % }
- <tr>
- <td class="<%= $td_class %>">
- % if ($checkin_from) {
- <a class="<%= $link_class %>" data-station="<%= $train->station_uic %>" data-train="<%= $train->train_id %>" data-dest="<%= $via %>"><%= $train->line %></a>
+ % if ($via_arr) {
+ → <%= $via_arr %>
% }
- % else {
- %= $train->line
+ % if ($train->departure_delay) {
+ %= sprintf('(%+d)', $train->departure_delay)
% }
- </td>
- <td class="<%= $td_class %>">
- % if ($checkin_from) {
- <a class="<%= $link_class %>" data-station="<%= $train->station_uic %>" data-train="<%= $train->train_id %>" data-dest="<%= $via %>"><%= $via %></a>
+ </a>
+ <span class="connect-platform-wrapper">
+ % if ($train->platform) {
+ <span>Gleis <%= $train->platform %></span>
+ % }
+ <span class="dep-line <%= $train->type // q{} %>">
+ %= $train->line
+ </span>
+ </span>
+ <span class="dep-dest">
+ % if ($train->is_cancelled) {
+ Fahrt nach <%= $via->{name} %> entfällt
% }
% else {
- %= $via
+ %= $via->{name}
% }
- </td>
- <td>
- % if ($train->departure_is_cancelled) {
- %= $train->sched_departure->strftime('%H:%M')
+ <br/>
+ % if ($load) {
+ % my ($first, $second) = load_icon($load);
+ <i class="material-icons tiny" aria-hidden="true"><%= $first %></i> <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
% }
- % else {
- %= $train->departure->strftime('%H:%M')
- % if ($train->departure_delay) {
- %= sprintf('(%+d)', $train->departure_delay)
- % }
+ % if ($train->{interchange_icon}) {
+ <i class="material-icons tiny" aria-label="<%= $train->{interchange_text} %>"><%= $train->{interchange_icon} %></i>
% }
- </td><td>
- % if (not $train->departure_is_cancelled) {
- Gleis <%= $train->platform || '?' %>
+ % if ($train->{message_id}{96} or $train->{message_id}{97}) {
+ <i class="material-icons tiny" aria-label="Zug ist überbesetzt">warning</i>
% }
- % else {
- fällt aus
+ % if ($train->{message_id}{82} or $train->{message_id}{85}) {
+ <i class="material-icons tiny" aria-label="Fehlende Wagen">remove</i>
+ % }
+ % if (($train->{message_id}{73} or $train->{message_id}{74} or $train->{message_id}{75} or $train->{message_id}{76} or $train->{message_id}{80}) and not $train->{message_id}{84}) {
+ <i class="material-icons tiny" aria-label="Abweichende Wagenreihung">compare_arrows</i>
+ % }
+ % if ($train->{message_id}{83} or $train->{message_id}{93} or $train->{message_id}{95}) {
+ <i class="material-icons tiny" aria-label="Eingeschränkte Barrierefreiheit">info_outline</i>
+ % }
+ % if ($train->{message_id}{70} or $train->{message_id}{71}) {
+ <i class="material-icons tiny" aria-label="Ohne WLAN">portable_wifi_off</i>
% }
- </td>
- </tr>
+ </span>
+ </li>
% }
-</tbody></table></div>
+</ul>
diff --git a/templates/_connections_hafas.html.ep b/templates/_connections_hafas.html.ep
new file mode 100644
index 0000000..dcf7ec9
--- /dev/null
+++ b/templates/_connections_hafas.html.ep
@@ -0,0 +1,48 @@
+<ul class="collection departures connections">
+ % for my $res (@{$connections}) {
+ % my ($train, $via, $via_arr) = @{$res};
+ % $via_arr = $via_arr ? $via_arr->strftime('%H:%M') : q{};
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($train->is_cancelled) {
+ % $row_class = 'cancelled';
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if ($checkin_from) {
+ <li class="collection-item <%= $row_class %> <%= $link_class %>"
+ data-station="<%= $train->station_eva %>"
+ data-train="<%= $train->id %>"
+ data-ts="<%= ($train->sched_datetime // $train->datetime)->epoch %>"
+ data-dest="<%= $via->{name} %>">
+ % }
+ % else {
+ <li class="collection-item <%= $row_class %>">
+ % }
+ <a class="dep-time" href="#">
+ % if ($train->is_cancelled) {
+ %= $train->sched_datetime->strftime('%H:%M')
+ % }
+ % else {
+ %= $train->datetime->strftime('%H:%M')
+ % }
+ % if ($via_arr) {
+ → <%= $via_arr %>
+ % }
+ % if ($train->delay) {
+ %= sprintf('(%+d)', $train->delay)
+ % }
+ </a>
+ <span class="connect-platform-wrapper">
+ % if ($train->platform) {
+ <span>Gleis <%= $train->platform %></span>
+ % }
+ <span class="dep-line <%= $train->type // q{} %>">
+ %= $train->line
+ </span>
+ </span>
+ <span class="dep-dest">
+ %= $via->{name}
+ </span>
+ </li>
+ % }
+</ul>
diff --git a/templates/_departures_hafas.html.ep b/templates/_departures_hafas.html.ep
new file mode 100644
index 0000000..9e4d7a4
--- /dev/null
+++ b/templates/_departures_hafas.html.ep
@@ -0,0 +1,53 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->datetime->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-station="<%= $result->station_eva %>"
+ data-train="<%= $result->id %>"
+ data-ts="<%= ($result->sched_datetime // $result->datetime)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ %= $result->datetime->strftime('%H:%M')
+ % if ($result->delay) {
+ (<%= sprintf('%+d', $result->delay) %>)
+ % }
+ % elsif (not defined $result->delay and not $result->is_cancelled) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= $result->type // q{} %>">
+ %= $result->line
+ </span>
+ <span class="dep-dest">
+ % if ($result->is_cancelled) {
+ Fahrt nach <%= $result->destination %> entfällt
+ % }
+ % else {
+ %= $result->destination
+ % for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_departures_iris.html.ep b/templates/_departures_iris.html.ep
new file mode 100644
index 0000000..d96fd37
--- /dev/null
+++ b/templates/_departures_iris.html.ep
@@ -0,0 +1,58 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->departure_is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->departure->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-station="<%= $result->station_uic %>"
+ data-train="<%= $result->train_id %>"
+ data-ts="<%= ($result->sched_departure // $result->departure)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ % if ($result->departure_hidden) {
+ (<%= $result->departure->strftime('%H:%M') %>)
+ % }
+ % else {
+ %= $result->departure->strftime('%H:%M')
+ % }
+ % if ($result->departure_delay) {
+ (<%= sprintf('%+d', $result->departure_delay) %>)
+ % }
+ % elsif (not $result->has_realtime and $result->start->epoch < $now_epoch) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= $result->type // q{} %>">
+ %= $result->line
+ </span>
+ <span class="dep-dest">
+ % if ($result->departure_is_cancelled) {
+ Fahrt nach <%= $result->destination %> entfällt
+ % }
+ % else {
+ %= $result->destination
+ % for my $checkin (@{$checkin_by_train->{$result->train_id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_footer.html.ep b/templates/_footer.html.ep
deleted file mode 100644
index 4f778c3..0000000
--- a/templates/_footer.html.ep
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="row" style="margin-top: 5em;">
- <div class="col s12 center-align grey-text">
- <a href="/impressum">Impressum</a>
- <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span>
- <a href="/impressum">Datenschutz</a>
- <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span>
- <a href="/about">travelynx</a> v<%= $version // '???' %>
- </div>
-</div>
diff --git a/templates/_format_train.html.ep b/templates/_format_train.html.ep
new file mode 100644
index 0000000..1d6acaa
--- /dev/null
+++ b/templates/_format_train.html.ep
@@ -0,0 +1,10 @@
+% if ($journey->{extra_data}{wagonorder_pride}) {
+ 🏳️‍🌈
+% }
+<span class="dep-line <%= $journey->{train_type} // q{} %>">
+ <%= $journey->{train_type} %>
+ <%= $journey->{train_line} // $journey->{train_no}%>
+</span>
+% if ($journey->{train_line}) {
+ <%= $journey->{train_no} %>
+% }
diff --git a/templates/_history_months.html.ep b/templates/_history_months.html.ep
index 6a83d74..2dc0141 100644
--- a/templates/_history_months.html.ep
+++ b/templates/_history_months.html.ep
@@ -1,12 +1,23 @@
<div class="row">
<div class="col s12">
<ul class="pagination">
- % for my $month (history_months()) {
- % my $link_to = $month->[0];
- % my $text = $month->[1];
- % my $class = $link_to eq $current ? 'active' : 'waves-effect';
- <li class="<%= $class %>"><a href="/history/<%= $link_to %>"><%= $text %></a></li>
+ % my ($prev, $current, $next) = journeys->get_nav_months(uid => current_user->{id}, year => $year, month => $month);
+ % if ($prev) {
+ <li class="waves-effect waves-light"><a href="/history/<%= $prev->[0] %>"><i class="material-icons">chevron_left</i></a></li>
% }
+ % else {
+ <li class="disabled"><a href="#!"><i class="material-icons">chevron_left</i></a></li>
+ % }
+ % if ($current) {
+ <li class="" style="min-width: 8em;"><a href="/history/<%= $current->[0] %>"><%= $current->[1] %></a></li>
+ % }
+ % if ($next) {
+ <li class="waves-effect waves-light"><a href="/history/<%= $next->[0] %>"><i class="material-icons">chevron_right</i></a></li>
+ % }
+ % else {
+ <li class="disabled"><a href="#!"><i class="material-icons">chevron_right</i></a></li>
+ % }
+ <li class=""><a href="/history/<%= $year %>"><%= $year %></a></li>
</ul>
</div>
</div>
diff --git a/templates/_history_months_for_year.html.ep b/templates/_history_months_for_year.html.ep
new file mode 100644
index 0000000..1d035ab
--- /dev/null
+++ b/templates/_history_months_for_year.html.ep
@@ -0,0 +1,18 @@
+<div class="row">
+ <div class="col s12">
+ <div class="collection">
+ % for my $month (journeys->get_months_for_year(uid => current_user->{id}, year => $year)) {
+ % if (defined $month->[2]) {
+ <a class="collection-item" href="/history/<%= $month->[0] %>"><%= $month->[1] %>
+ % if (defined $month->[2]{km_route}) {
+ <span class="secondary-content"><%= sprintf('%.f', $month->[2]{km_route}) %> km</span>
+ % }
+ </a>
+ % }
+ % else {
+ <div class="collection-item disabled"><%= $month->[1] %></div>
+ % }
+ % }
+ </div>
+ </div>
+</div>
diff --git a/templates/_history_stats.html.ep b/templates/_history_stats.html.ep
index 8197ed1..cbdbb13 100644
--- a/templates/_history_stats.html.ep
+++ b/templates/_history_stats.html.ep
@@ -1,35 +1,8 @@
-% if (@{$stats->{inconsistencies}}) {
- <div class="row">
- <div class="col s12">
- <div class="card caution-color">
- <div class="card-content white-text">
- <i class="material-icons small right">warning</i>
- <span class="card-title">Inkonsistente Reisedaten</span>
- <p>
- Die folgenden Abfahrtszeiten liegen vor der Ankunftszeit der
- vorherigen Zugfahrt und wurden bei der Wartezeitberechnung
- ignoriert.
- <ul>
- % for my $date (@{$stats->{inconsistencies}}) {
- <li><%= $date %></li>
- % }
- </ul>
- </p>
- </div>
- </div>
- </div>
- </div>
-% }
-
<div class="row">
<div class="col s12">
<table class="striped">
<tr>
<th scope="row">Fahrten</th>
- <td><%= $stats->{num_journeys} %></td>
- </tr>
- <tr>
- <th scope="row">Züge</th>
<td><%= $stats->{num_trains} %></td>
</tr>
<tr>
@@ -39,17 +12,25 @@
</tr>
<tr>
<th scope="row">Fahrtzeit</th>
- <td><%= sprintf('%02d:%02d', $stats->{min_travel_real} / 60, $stats->{min_travel_real} % 60) %> Stunden
- (nach Fahrplan: <%= sprintf('%02d:%02d', $stats->{min_travel_sched} / 60, $stats->{min_travel_sched} % 60) %>)<td>
+ <td><%= $stats->{min_travel_real_strf} %> Stunden
+ (nach Fahrplan: <%= $stats->{min_travel_sched_strf} %>)</td>
</tr>
<tr>
<th scope="row">Wartezeit (nur Umstiege)</th>
- <td><%= sprintf('%02d:%02d', $stats->{min_interchange_real} / 60, $stats->{min_interchange_real} % 60) %> Stunden
+ <td><%= $stats->{min_interchange_real_strf} %> Stunden
+ % if (@{$stats->{inconsistencies}}) {
+ <br/><br/>Für Wartezeitberechnung nicht berücksichtigte Fahrten:<br/>
+ % for my $field (@{$stats->{inconsistencies}}) {
+ <a href="/journey/<%= $field->{ignored}{id} %>"><%= $field->{ignored}{train} %> ab <%= $field->{ignored}{dep} %></a>
+ (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: <%= sprintf('%02d:%02d', $stats->{delay_dep} / 60, $stats->{delay_dep} % 60) %> Stunden<br/>
- Bei Ankunft: <%= sprintf('%02d:%02d', $stats->{delay_arr} / 60, $stats->{delay_arr} % 60) %> Stunden</td>
+ <td>Bei Abfahrt: <%= $stats->{delay_dep_strf} %> Stunden<br/>
+ Bei Ankunft: <%= $stats->{delay_arr_strf} %> Stunden</td>
</tr>
</table>
</div>
diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep
index d422165..cf998ab 100644
--- a/templates/_history_trains.html.ep
+++ b/templates/_history_trains.html.ep
@@ -1,36 +1,30 @@
<div class="row">
<div class="col s12">
- <table class="striped">
- <thead>
- <tr>
- <th>Datum</th>
- <th>Zug</th>
- <th>Von</th>
- <th>Nach</th>
- </tr>
- </thead>
- <tbody>
- % for my $travel (@{$journeys}) {
- % my $detail_link = '/journey/' . $travel->{id};
- <tr>
- <td><%= $travel->{sched_departure}->strftime($date_format) %></td>
- <td><a href="<%= $detail_link %>"><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></a></td>
- <td>
- <a href="<%= $detail_link %>" class="unmarked">
- % if (param('cancelled')) {
- %= $travel->{sched_departure}->strftime('%H:%M')
- % }
- % 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) %>)
- % }
- % }
- <br/>
- <%= $travel->{from_name} %>
- </a>
- </td>
- <td>
+ <ul class="collection history">
+ % my $olddate = '';
+ % for my $travel (@{$journeys}) {
+ % my $detail_link = '/journey/' . $travel->{id};
+ % if (my $prefix = stash('link_prefix')) {
+ % $detail_link = $prefix . $travel->{id};
+ % }
+ % my $date = $travel->{sched_departure}->strftime($date_format);
+ % if ($olddate ne $date) {
+ <li class="collection-item history-date-change">
+ <b><%= $date %></b>
+ </li>
+ % $olddate = $date
+ % }
+ <li class="collection-item">
+ <a href="<%= $detail_link %>">
+ <span class="dep-line <%= $travel->{type} // q{} %>">
+ <%= $travel->{type} %> <%= $travel->{line} // $travel->{no}%>
+ </span>
+ </a>
+
+ <ul class="route-history">
+ <li>
+ <i class="material-icons tiny" aria-label="nach">radio_button_unchecked</i>
+
<a href="<%= $detail_link %>" class="unmarked">
% if (param('cancelled') and $travel->{sched_arrival}->epoch != 0) {
%= $travel->{sched_arrival}->strftime('%H:%M')
@@ -45,12 +39,29 @@
% }
% }
% }
- <br/>
- <%= $travel->{to_name} %>
- </a></td>
- </tr>
- % }
- </tbody>
- </table>
+ <strong><%= $travel->{to_name} %></strong>
+ </a>
+ </li>
+
+ <li>
+ <i class="material-icons tiny" aria-label="von">play_circle_filled</i>
+
+ <a href="<%= $detail_link %>" class="unmarked">
+ % if (param('cancelled')) {
+ %= $travel->{sched_departure}->strftime('%H:%M')
+ % }
+ % 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) %>)
+ % }
+ % }
+ <strong><%= $travel->{from_name} %></strong>
+ </a>
+ </li>
+ </ul>
+ </li>
+ % }
+ </ul>
</div>
</div>
diff --git a/templates/_history_years.html.ep b/templates/_history_years.html.ep
index 768c438..5591799 100644
--- a/templates/_history_years.html.ep
+++ b/templates/_history_years.html.ep
@@ -1,12 +1,18 @@
<div class="row">
<div class="col s12">
- <ul class="pagination">
- % for my $year (history_years()) {
- % my $link_to = $year->[0];
- % my $text = $year->[1];
- % my $class = $link_to eq $current ? 'active' : 'waves-effect';
- <li class="<%= $class %>"><a href="/history/<%= $link_to %>"><%= $text %></a></li>
- % }
- </ul>
+ % my @years = journeys->get_nav_years(uid => current_user->{id});
+ % if (@years) {
+ <ul class="pagination">
+ % for my $year (@years) {
+ % my $link_to = $year->[0];
+ % my $text = $year->[1];
+ % my $class = $link_to eq $current ? 'active' : 'waves-effect';
+ <li class="<%= $class %>"><a href="/history/<%= $link_to %>"><%= $text %></a></li>
+ % }
+ </ul>
+ % }
+ % else {
+ Keine Fahrten gefunden.
+ % }
</div>
</div>
diff --git a/templates/_history_years_list.html.ep b/templates/_history_years_list.html.ep
new file mode 100644
index 0000000..e91ebca
--- /dev/null
+++ b/templates/_history_years_list.html.ep
@@ -0,0 +1,13 @@
+<div class="row">
+ <div class="col s12">
+ <div class="collection">
+ % for my $year (journeys->get_years(uid => current_user->{id})) {
+ <a class="collection-item" href="/history/<%= $year->[0] %>"><%= $year->[1] %>
+ % if (defined $year->[2]{km_route}) {
+ <span class="secondary-content"><%= sprintf('%.f', $year->[2]{km_route}) %> km</span>
+ % }
+ </a>
+ % }
+ </div>
+ </div>
+</div>
diff --git a/templates/_invalid_input.html.ep b/templates/_invalid_input.html.ep
index 4cebf29..f8c4e2f 100644
--- a/templates/_invalid_input.html.ep
+++ b/templates/_invalid_input.html.ep
@@ -2,14 +2,7 @@
<div class="col s12">
<div class="card caution-color">
<div class="card-content white-text">
- % if ($invalid eq 'csrf') {
- <span class="card-title">Ungültiger CSRF-Token</span>
- <p>Sind Cookies aktiviert? Ansonsten könnte es sich um einen
- Fall von <a
- href="https://de.wikipedia.org/wiki/Cross-Site-Request-Forgery">CSRF</a>
- handeln.</p>
- % }
- % elsif ($invalid eq 'credentials') {
+ % if ($invalid eq 'credentials') {
<span class="card-title">Ungültige Logindaten</span>
<p>Falscher Account oder falsches Passwort.</p>
% }
@@ -78,6 +71,10 @@
<p>Aus Sicherheitsgründen kann der Account nur nach Passworteingabe
gelöscht werden.</p>
% }
+ % elsif ($invalid eq 'denylist') {
+ <span class="card-title">Registrierung deaktiviert</span>
+ <p>Für diesen Zugang ist derzeit keine Registrierung möglich.</p>
+ % }
% else {
<span class="card-title">Fehler</span>
<p><%= $invalid %></p>
diff --git a/templates/_map.html.ep b/templates/_map.html.ep
index cceec01..daa16f0 100644
--- a/templates/_map.html.ep
+++ b/templates/_map.html.ep
@@ -1,6 +1,6 @@
<div class="row">
<div class="col s12">
- <div id="map" style="height: 500px;">
+ <div id="map" style="height: 70vh;">
</div>
</div>
</div>
@@ -8,7 +8,7 @@
<div class="row">
<div class="col s12">
<span style="color: #f03;">●</span> Ein-/Ausstiegsstation<br/>
- <span style="color: #f09;">—</span> Luftlinie zwischen Unterwegshalten
+ <span style="color: #673ab7;">—</span> Streckenverlauf oder Luftlinie
</div>
</div>
@@ -25,24 +25,30 @@ var stations = [
% }
];
-var routes = [
-% for my $pair ( @{stash('station_pairs') // [] } ) {
-[[<%= $pair->[0][0] %>,<%= $pair->[0][1] %>],[<%= $pair->[1][0] %>,<%= $pair->[1][1] %>]],
+var routes = [];
+var pl;
+% for my $line_group ( @{ stash('polyline_groups') // [] } ) {
+ routes = [ <%= $line_group->{polylines} %> ];
+ pl = L.polyline(routes, {color: '<%= $line_group->{color} %>', opacity: <%= $line_group->{opacity} %>}).addTo(map);
+ % if ($line_group->{fit_bounds}) {
+ if (routes.length) {
+ map.fitBounds(pl.getBounds());
+ }
+ % }
+% }
+
+% if (my $b = stash('bounds')) {
+ map.fitBounds([[<%= $b->[0][0] %>,<%= $b->[0][1] %>],[<%= $b->[1][0] %>,<%= $b->[1][1] %>]]);
% }
-];
for (var station_id in stations) {
L.circle(stations[station_id][0], {
color: '#f03',
+ opacity: 0.7,
fillColor: '#f03',
fillOpacity: 0.5,
radius: 250
}).bindPopup(stations[station_id][1]).addTo(map);
}
-var pl = L.polyline(routes, {color: '#f09', opacity: 0.5}).addTo(map);
-if (routes.length) {
- map.fitBounds(pl.getBounds());
-}
-
</script>
diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep
index 2ff7ac1..b463d15 100644
--- a/templates/_public_status_card.html.ep
+++ b/templates/_public_status_card.html.ep
@@ -1,36 +1,47 @@
-<div class="autorefresh">
+<div class="autorefresh" data-from-profile="<%= stash('from_profile') ? 1 : 0 %>">
% if ($journey->{checked_in}) {
<div class="card">
<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"><%= $name %> ist unterwegs</span>
- % if ($public_level & 0x04 and $journey->{comment}) {
+ <i class="material-icons right sync-failed-marker grey-text" style="display: none;">sync_problem</i>
+ <span class="card-title">
+ % if (stash('from_profile')) {
+ Unterwegs mit <%= include '_format_train', journey => $journey %>
+ % }
+ % elsif (stash('from_timeline')) {
+ <a href="/p/<%= $name %>"><%= $name %></a>: <%= include '_format_train', journey => $journey %>
+ % }
+ % else {
+ <a href="/p/<%= $name %>"><%= $name %></a> ist unterwegs
+ % }
+ <i class="material-icons right"><%= visibility_icon($journey->{effective_visibility_str}) %></i>
+ % if (not $journey->{extra_data}{rt}) {
+ <i class="material-icons right grey-text">gps_off</i>
+ % }
+ </span>
+ % if ($privacy->{comments_visible} and $journey->{comment}) {
<p>„<%= $journey->{comment} %>“</p>
% }
<p>
- % my $url = 'https://marudor.de/details/' . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . DateTime->now(time_zone => 'Europe/Berlin')->epoch . '000';
- % if ($journey->{train_line}) {
- <div class="center-align"><a href="<%= $url %>"><b><%= $journey->{train_type} %> <%= $journey->{train_line} %></b> <%= $journey->{train_no} %></a></div>
- % }
- % else {
- <div class="center-align"><a href="<%= $url %>"><b><%= $journey->{train_type} %> <%= $journey->{train_no} %></b></a></div>
+ % if (not stash('from_profile') and not stash('from_timeline')) {
+ <div class="center-align">
+ %= include '_format_train', journey => $journey
+ </div>
% }
<div class="center-align countdown"
data-duration="<%= $journey->{journey_duration} // 0 %>"
- data-arrival="<%= $journey->{real_arrival}->epoch %>">
- % if ($journey->{departure_countdown} > 120) {
- Abfahrt in <%= sprintf('%.f', $journey->{departure_countdown} / 60) %> Minuten
+ % if (param('token')) {
+ data-token="<%= $journey->{dep_eva} %>-<%= $journey->{timestamp}->epoch % 337 %>-<%= $journey->{sched_departure}->epoch %>"
% }
- % elsif ($journey->{departure_countdown} > 60) {
- Abfahrt in einer Minute
+ data-arrival="<%= $journey->{real_arrival}->epoch %>">
+ % if ($journey->{departure_countdown} > 60) {
+ Abfahrt in <%= journeys->min_to_human(int($journey->{departure_countdown} / 60)) %>
% }
% elsif ($journey->{departure_countdown} > 0) {
Abfahrt in weniger als einer Minute
% }
% elsif (defined $journey->{arrival_countdown}) {
% if ($journey->{arrival_countdown} > 60) {
- Ankunft in <%= sprintf('%.f', $journey->{arrival_countdown} / 60) %>
- Minute<%= sprintf('%.f', $journey->{arrival_countdown} / 60) == 1 ? '' : 'n' %>
+ Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %>
% }
% elsif ($journey->{arrival_countdown} > 0) {
Ankunft in weniger als einer Minute
@@ -80,19 +91,21 @@
% if ($journey->{arr_name} and $station->[0] eq $journey->{arr_name}) {
% last;
% }
- % if (($station->[1]{rt_arr_countdown} // 0) > 0) {
- <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %>
- % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60);
+ % if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
+ <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ % if ($station->[2]{arr_delay}) {
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
% }
% last;
% }
- % if (($station->[1]{rt_dep_countdown} // 0) > 0) {
+ % if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{dep}) {
<%= $station->[0] %><br/>
- <%= $station->[1]{rt_arr}->strftime('%H:%M') %> →
- <%= $station->[1]{rt_dep}->strftime('%H:%M') %>
- % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60);
+ % if ($station->[2]{arr}) {
+ <%= $station->[2]{arr}->strftime('%H:%M') %> →
+ % }
+ %= $station->[2]{dep}->strftime('%H:%M')
+ % if ($station->[2]{dep_delay}) {
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
% }
% last;
% }
@@ -105,27 +118,34 @@
% if ($journey->{arr_name} and $station->[0] eq $journey->{arr_name}) {
% last;
% }
- % if (($station->[1]{rt_arr_countdown} // 0) > 0) {
+ % if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
Nächster Halt:<br/>
- <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %>
- % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60);
+ <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ % if ($station->[2]{arr_delay}) {
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
% }
% last;
% }
- % if (($station->[1]{rt_dep_countdown} // 0) > 0) {
+ % if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{arr} and $station->[2]{dep}) {
Aktueller Halt:<br/>
<%= $station->[0] %><br/>
- <%= $station->[1]{rt_arr}->strftime('%H:%M') %> →
- <%= $station->[1]{rt_dep}->strftime('%H:%M') %>
- % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) {
- %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60);
+ <%= $station->[2]{arr}->strftime('%H:%M') %> →
+ <%= $station->[2]{dep}->strftime('%H:%M') %>
+ % if ($station->[2]{dep_delay}) {
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
% }
% last;
% }
% }
</div>
</p>
+ % if ($journey->{extra_data}{cancelled_destination}) {
+ <p style="margin-bottom: 2ex;">
+ <i class="material-icons tiny" aria-hidden="true">error</i>
+ Der Halt an der Zielstation <b><%=
+ $journey->{extra_data}{cancelled_destination} %></b> entfällt.
+ </p>
+ % }
% if (@{$journey->{messages} // []} > 0 and $journey->{messages}[0]) {
<p style="margin-bottom: 2ex;">
<ul>
@@ -134,19 +154,50 @@
<li> <i class="material-icons tiny">warning</i> <%= $message->[0]->strftime('%H:%M') %>: <%= $message->[1] %></li>
% }
% }
+ % for my $message (@{$journey->{extra_data}{qos_msg} // []}) {
+ <li> <i class="material-icons tiny">info</i> <%= $message->[0]->strftime('%H:%M') %>: <%= $message->[1] %></li>
+ % }
</ul>
</p>
% }
</div>
+ % if (not stash('from_timeline')) {
+ <div class="card-action">
+ % if ($journey->{traewelling_url}) {
+ <a style="margin-right: 0;" href="<%= $journey->{traewelling_url} %>"><i class="material-icons left">timeline</i> Träwelling</a>
+ % } else {
+ % my $url = 'https://bahn.expert/details/' . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . DateTime->now(time_zone => 'Europe/Berlin')->epoch . '000';
+ <a style="margin-right: 0;" href="<%= $url %>"><i class="material-icons left">timeline</i> Zuglauf</a>
+ % }
+ % if ($journey->{extra_data}{trip_id}) {
+ <a class="right" style="margin-right: 0;" href="https://dbf.finalrewind.org/map/<%= $journey->{extra_data}{trip_id} %>/<%= $journey->{train_line} || 0 %>?from=<%= $journey->{dep_name} %>&amp;to=<%= $journey->{arr_name} // '' %>"><i class="material-icons left">map</i> Karte</a>
+ % }
+ </div>
+ % }
</div>
% }
% else {
<div class="card">
<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"><%= $name %> ist gerade nicht eingecheckt</span>
+ % if (stash('from_profile')) {
+ <span class="card-title">Aktuell nicht eingecheckt</span>
+ % }
+ % else {
+ <span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> ist gerade nicht eingecheckt</span>
+ % }
<p>
- Zuletzt gesehen in <%= $journey->{arr_name} %>.
+ % if ($journey->{arr_name}) {
+ Zuletzt gesehen
+ % if ($journey->{real_arrival}->epoch) {
+ %= $journey->{real_arrival}->strftime('am %d.%m.%Y')
+ in <b><%= $journey->{arr_name} %></b>
+ %= $journey->{real_arrival}->strftime('(Ankunft um %H:%M Uhr)')
+ % }
+ % else {
+ in <b><%= $journey->{arr_name} %></b>
+ % }
+ % }
</p>
</div>
</div>
diff --git a/templates/_timeline-checked-in.html.ep b/templates/_timeline-checked-in.html.ep
new file mode 100644
index 0000000..10c5e26
--- /dev/null
+++ b/templates/_timeline-checked-in.html.ep
@@ -0,0 +1,14 @@
+% for my $journey (@{$journeys}) {
+ <div class="row">
+ <div class="col s12 autorefresh">
+ %= include '_public_status_card', name => $journey->{followee_name}, privacy => {}, journey => $journey, from_timeline => 1
+ </div>
+ </div>
+% }
+% if (not @{$journeys}) {
+ <div class="row">
+ <div class="col s12 autorefresh center-align">
+ <i>Gerade sind keine Accounts mit für dich sichtbaren Checkins unterwegs</i>
+ </div>
+ </div>
+% }
diff --git a/templates/_timeline_link.html.ep b/templates/_timeline_link.html.ep
new file mode 100644
index 0000000..1a78279
--- /dev/null
+++ b/templates/_timeline_link.html.ep
@@ -0,0 +1,16 @@
+<div>
+ <a href="/timeline/in-transit">
+ % if (@{$timeline} <= 2) {
+ <strong><%= $timeline->[0]->{followee_name} %></strong>
+ % }
+ % if (@{$timeline} == 1) {
+ ist gerade <%= stash('from_checkin') ? 'auch' : q{} %> unterwegs
+ % }
+ % elsif (@{$timeline} == 2) {
+ und <strong><%= $timeline->[1]->{followee_name} %></strong> sind gerade <%= stash('from_checkin') ? 'auch' : q{} %> unterwegs
+ % }
+ % else {
+ <strong><%= scalar @{$timeline} %></strong> Accounts sind gerade <%= stash('from_checkin') ? 'auch' : q{} %> unterwegs
+ % }
+ </a>
+</div>
diff --git a/templates/_wagons.html.ep b/templates/_wagons.html.ep
index 8918273..106709e 100644
--- a/templates/_wagons.html.ep
+++ b/templates/_wagons.html.ep
@@ -1,5 +1,11 @@
% for my $wagongroup (@{$wagongroups // []}) {
- Wagenverbund <%= $wagongroup->{name} %> von <b><%= $wagongroup->{from} %></b> nach <b><%= $wagongroup->{to} %></b> als <b><%= $journey->{type} %> <%= $wagongroup->{no} %></b><br/>
+ <%= $wagongroup->{name} %>
+ % my ($wagon_number) = ($wagongroup->{name} =~ m{ ^ ICE 0* (\d+) $ }x);
+ % if ($wagon_number and my $group_name = app->ice_name->{$wagon_number}) {
+ „<%= $group_name %>“
+ % }
+ als <b><%= $journey->{type} %> <%= $wagongroup->{no} %></b>
+ von <b><%= $wagongroup->{from} %></b> nach <b><%= $wagongroup->{to} %></b><br/>
% for my $wagon (@{$wagongroup->{wagons}}) {
% if (length($wagon->{id}) == 12) {
<span><%= substr($wagon->{id}, 0, 2) %></span><span><%= substr($wagon->{id}, 2, 2) %></span><span><%= substr($wagon->{id}, 4, 1) %></span><span class="wagonclass"><%= substr($wagon->{id}, 5, 3) %></span><span class="wagonnum"><%= substr($wagon->{id}, 8, 3) %></span><span class="checksum"><%= substr($wagon->{id}, 11) %></span>
diff --git a/templates/about.html.ep b/templates/about.html.ep
index 17424f1..ea86bdf 100644
--- a/templates/about.html.ep
+++ b/templates/about.html.ep
@@ -1,10 +1,13 @@
<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://twitter.com/derfnull">@derfnull</a><br/>
- Backend:
+ Entwickelt von <a href="https://finalrewind.org">derf</a><br/>
+ <a href="<%= app->config->{ref}{source} // 'https://github.com/derf/travelynx' %>">Quelltext</a> lizensiert unter AGPL v3<br/><br/>
+ Backends:
<a href="https://finalrewind.org/projects/Travel-Status-DE-IRIS/">Travel::Status::DE::IRIS</a>
- v<%= $Travel::Status::DE::IRIS::VERSION %><br/>
+ v<%= $Travel::Status::DE::IRIS::VERSION %> und
+ <a href="https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/">Travel::Status::DE::HAFAS</a>
+ v<%= $Travel::Status::DE::HAFAS::VERSION %><br/>
<a href="http://data.deutschebahn.com/dataset/data-haltestellen">Haltestellendaten</a>
© DB Station&amp;Service AG,
Europaplatz 1,
@@ -14,10 +17,12 @@
<div class="row">
<div class="col s12 m12 l4 center-align" style="margin-top: 1em;">
- <a href="https://twitter.com/derfnull" 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>Kontakt</a>
</div>
<div class="col s12 m12 l4 center-align" style="margin-top: 1em;">
- <a href="https://github.com/derf/travelynx/issues" class="waves-effect waves-light btn"><i class="material-icons left">bug_report</i>Bugs?</a>
+ % 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>
+ % }
</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>
diff --git a/templates/account.html.ep b/templates/account.html.ep
index e6a4576..7f689c2 100644
--- a/templates/account.html.ep
+++ b/templates/account.html.ep
@@ -1,4 +1,4 @@
-% if (my $invalid = stash('invalid')) {
+% if (my $invalid = flash('invalid')) {
%= include '_invalid_input', invalid => $invalid
% }
@@ -7,7 +7,10 @@
<div class="col s12">
<div class="card success-color">
<div class="card-content white-text">
- % if ($success eq 'mail') {
+ % if ($success eq 'name') {
+ <span class="card-title">Name geändert</span>
+ % }
+ % elsif ($success eq 'mail') {
<span class="card-title">Mail-Adresse geändert</span>
% }
% elsif ($success eq 'password') {
@@ -16,28 +19,41 @@
% elsif ($success eq 'privacy') {
<span class="card-title">Einstellungen zu öffentlichen Account-Daten geändert</span>
% }
+ % elsif ($success eq 'social') {
+ <span class="card-title">Einstellungen zur Interaktionen mit anderen Accounts geändert</span>
+ % }
+ % elsif ($success eq 'traewelling') {
+ <span class="card-title">Träwelling-Verknüpfung aktualisiert</span>
+ % }
% elsif ($success eq 'use_history') {
<span class="card-title">Einstellungen zu vorgeschlagenen Verbindungen geändert</span>
% }
+ % elsif ($success eq 'external') {
+ <span class="card-title">Einstellungen zu externen Diensten geändert</span>
+ % }
% elsif ($success eq 'webhook') {
<span class="card-title">Web Hook aktualisiert</span>
% }
+ % elsif ($success eq 'clear_notifications') {
+ <span class="card-title">Benachrichtigungen gelesen</span>
+ % }
</div>
</div>
</div>
</div>
% }
-<h1>Account</h1>
% my $acc = current_user();
-% my $hook = get_webhook();
-% my $use_history = account_use_history($acc->{id});
+% my $hook = users->get_webhook(uid => $acc->{id});
+% my $traewelling = traewelling->get(uid => $acc->{id});
+% my $use_history = users->use_history(uid => $acc->{id});
<div class="row">
<div class="col s12">
+ <h2>Account</h2>
<table class="striped">
<tr>
<th scope="row">Name</th>
- <td><%= $acc->{name} %></td>
+ <td><a href="/account/name"><i class="material-icons">edit</i></a><%= $acc->{name} %></td>
</tr>
<tr>
<th scope="row">Mail</th>
@@ -60,20 +76,31 @@
</td>
</tr>
<tr>
- <th scope="row">Öffentliche Daten</th>
+ <th scope="row">Sichtbarkeit</th>
<td>
<a href="/account/privacy"><i class="material-icons">edit</i></a>
- % if ($acc->{is_public} == 0) {
- <span style="color: #999999;">Keine</span>
- % }
- % if ($acc->{is_public} & 0x01) {
- Aktueller Status (nur für angemeldete Accounts)
+ <i class="material-icons">check</i><i class="material-icons"><%= visibility_icon($acc->{default_visibility_str}) %></i>
+ • <i class="material-icons">history</i><i class="material-icons"><%= visibility_icon($acc->{past_visibility_str}) %></i>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Interaktion</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>
% }
- % elsif ($acc->{is_public} & 0x02) {
- Aktueller Status
+ % elsif ($acc->{accept_follow_requests}) {
+ <span>Accounts können dir auf Anfrage folgen
+ % if ($num_rx_follow_requests == 1) {
+ – <a href="/account/social/follow-requests-received"><strong>eine</strong> offene Anfrage</a>
+ % } elsif ($num_rx_follow_requests) {
+ – <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> offene Anfragen</a>
+ % }
+ </span>
% }
- % if ($acc->{is_public} & 0x04) {
- mit Kommentar
+ % else {
+ <span style="color: #999999;">Accounts können dir nicht folgen</span>
% }
</td>
</tr>
@@ -88,13 +115,54 @@
Aktiv, noch nicht ausgeführt
% }
% elsif ($hook->{errored}) {
- Aktiv, fehlerhaft <i class="material-icons">error</i>
+ Aktiv, fehlerhaft <i class="material-icons" aria-hidden="true">error</i>
% }
% else {
Aktiv
% }
</td>
</tr>
+ % if (config->{traewelling}{oauth}) {
+ <tr>
+ <th scope="row">Träwelling</th>
+ <td>
+ <a href="/account/traewelling"><i class="material-icons">edit</i></a>
+ % if (not ($traewelling->{token})) {
+ <span style="color: #999999;">Nicht verknüpft</span>
+ % }
+ % elsif ($traewelling->{errored}) {
+ Fehlerhaft <i class="material-icons" aria-hidden="true">error</i>
+ % }
+ % else {
+ Verknüpft mit <%= $traewelling->{data}{user_name} // $traewelling->{email} %>
+ % if ($traewelling->{expired}) {
+ – Login-Token abgelaufen <i class="material-icons" aria-hidden="true">error</i>
+ % }
+ % elsif ($traewelling->{expiring}) {
+ – Login-Token läuft bald ab <i class="material-icons" aria-hidden="true">warning</i>
+ % }
+ % elsif ($traewelling->{pull_sync}) {
+ – Checkins in Träwelling werden von travelynx übernommen
+ % }
+ % elsif ($traewelling->{push_sync}) {
+ – Checkins in travelynx werden zu Träwelling weitergereicht
+ % }
+ % }
+ </td>
+ </tr>
+ % }
+ <tr>
+ <th scope="row">Externe Dienste</th>
+ <td>
+ <a href="/account/services"><i class="material-icons">edit</i></a>
+ % if ($acc->{sb_name}) {
+ Abfahrtstafel: <%= $acc->{sb_name} %>
+ % }
+ % else {
+ <span style="color: #999999;">Keine</span>
+ % }
+ </td>
+ </tr>
<tr>
<th scope="row">Registriert am</th>
<td><%= $acc->{registered_at}->strftime('%d.%m.%Y %H:%M') %></td>
@@ -113,15 +181,101 @@
</div>
</div>
-<h2>API</h2>
-% my $token = get_api_token();
+% 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>
+ <p>
+ <a href="/p/<%= $acc->{name} %>">Öffentliches Profil</a>
+ </p>
+ <table class="striped">
+ <tr>
+ <th scope="row">Anfragen</th>
+ <td>
+ % if ($num_rx_follow_requests == 0) {
+ <span style="color: #999999;">keine eingehend</span>
+ % }
+ % elsif ($num_rx_follow_requests == 1) {
+ <a href="/account/social/follow-requests-received"><strong>eine</strong> eingehend</a>
+ % }
+ % else {
+ <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> eingehend</a>
+ % }
+ <br/>
+ % if ($num_tx_follow_requests == 0) {
+ <span style="color: #999999;">keine ausgehend</span>
+ % }
+ % elsif ($num_tx_follow_requests == 1) {
+ <a href="/account/social/follow-requests-sent"><strong>eine</strong> ausgehend</a>
+ % }
+ % else {
+ <a href="/account/social/follow-requests-sent"><strong><%= $num_tx_follow_requests %></strong> ausgehend</a>
+ % }
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Dir folg<%= $num_followers == 1 ? 't' : 'en' %></th>
+ <td>
+ % if ($num_followers == 0) {
+ <span style="color: #999999;">keine Accounts</span>
+ % }
+ % elsif ($num_followers == 1) {
+ <a href="/account/social/followers"><strong>ein</strong> Account</a>
+ % }
+ % else {
+ <a href="/account/social/followers"><strong><%= $num_followers %></strong> Accounts</a>
+ % }
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Du folgst</th>
+ <td>
+ % if ($num_following == 0) {
+ <span style="color: #999999;">keinen Accounts</span>
+ % }
+ % elsif ($num_following == 1) {
+ <a href="/account/social/follows"><strong>einem</strong> Account</a>
+ % }
+ % else {
+ <a href="/account/social/follows"><strong><%= $num_following %></strong> Accounts</a>
+ % }
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Blockiert</th>
+ <td>
+ % if ($num_blocked == 0) {
+ <span style="color: #999999;">keine Accounts</span>
+ % }
+ % elsif ($num_blocked == 1) {
+ <a href="/account/social/blocks"><strong>ein</strong> Account</a>
+ % }
+ % else {
+ <a href="/account/social/blocks"><strong><%= $num_blocked %></strong> Accounts</a>
+ % }
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+% }
+% else {
+ <div class="row">
+ <div class="col s12">
+ <a href="/p/<%= $acc->{name} %>">Öffentliches Profil</a>
+ </div>
+ </div>
+% }
+
+% my $token = stash('api_token') // {};
<div class="row">
<div class="col s12">
+ <h2>API</h2>
<p>
Die folgenden API-Token erlauben den passwortlosen automatisierten Zugriff auf
API-Endpunkte. Bitte umsichtig behandeln – sobald ein Token gesetzt
ist, können damit ohne Logindaten alle zugehörigen API-Aktionen
- ausgeführt werden.
+ ausgeführt werden. <a href="/api">Dokumentation</a>.
</p>
<table class="striped">
<tr>
@@ -170,7 +324,6 @@
%= end
</td>
</tr>-->
- % if (app->mode eq 'development') {
<tr>
<th scope="row">Travel</th>
<td>
@@ -217,22 +370,13 @@
%= end
</td>
</tr>
- % }
</table>
</div>
</div>
<div class="row">
<div class="col s12">
- <a href="/api">Dokumentation</a>
- </div>
-</div>
-
-
-<h2>Export</h2>
-
-<div class="row">
- <div class="col s12">
+ <h2>Export</h2>
<ul>
<li><a href="/export.json">Rohdaten</a> (Kein API-Ersatz, das Format kann sich jederzeit ändern)</li>
</ul>
@@ -240,9 +384,9 @@
</div>
% if (not $acc->{deletion_requested}) {
- <h2>Löschen</h2>
<div class="row">
<div class="col s12">
+ <h2>Löschen</h2>
<p>
Der Löschauftrag wird vorgemerkt und erst nach drei Tagen
umgesetzt, bis dahin kann er jederzeit zurückgenommen werden. Nach
diff --git a/templates/add_journey.html.ep b/templates/add_journey.html.ep
index c3bc01c..c543781 100644
--- a/templates/add_journey.html.ep
+++ b/templates/add_journey.html.ep
@@ -1,13 +1,15 @@
-<h1>Zugfahrt eingeben</h1>
-% if (not get_oldest_journey_ts()) {
+<h1>Fahrt eingeben</h1>
+% if (not journeys->get_oldest_ts(uid => current_user->{id})) {
<div class="row">
<div class="col s12">
<div class="card info-color">
<div class="card-content">
<span class="card-title">Hinweis</span>
<p>travelynx ist darauf ausgelegt, über die Hauptseite in
- Echtzeit in Züge ein- und auszuchecken. Die manuelle
- Eingabe von Zugfahrten ist nur als Notlösung vorgesehen.</p>
+ Echtzeit in Verkehrsmittel ein- und auszuchecken. Die manuelle
+ Eingabe von Fahrten ist nur als Notlösung vorgesehen.
+ Hier werden derzeit nur Zugfahrten im DB-Netz
+ (IRIS-Backend) unterstützt.</p>
</div>
</div>
</div>
@@ -28,8 +30,9 @@
<div class="row">
<div class="col s12">
<ul>
- <li>Eingabe des Zugs als „Zug Typ Nummer“ oder „Zug Nummer“, z.B.
+ <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>
</ul>
</div>
@@ -39,7 +42,7 @@
<div class="row">
<div class="input-field col s12 m6 l6">
%= text_field 'train', id => 'train', class => 'validate', required => undef, pattern => '[0-9a-zA-Z]+ +[0-9a-zA-Z]* *[0-9]+'
- <label for="train">Zug (Typ Linie Nummer)</label>
+ <label for="train">Fahrt (Typ Linie Nummer)</label>
</div>
<div class="input-field col s12 m6 l6">
<label>
@@ -50,7 +53,7 @@
</div>
<div class="row">
<div class="input-field col s12">
- %= text_field 'dep_station', id => 'dep_station', class => 'autocomplete validate', required => undef
+ %= text_field 'dep_station', id => 'dep_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
<label for="dep_station">Start (Name oder DS100)</label>
</div>
<div class="input-field col s12">
@@ -64,7 +67,7 @@
</div>
<div class="row">
<div class="input-field col s12">
- %= text_field 'arr_station', id => 'arr_station', class => 'autocomplete validate', required => undef
+ %= text_field 'arr_station', id => 'arr_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
<label for="arr_station">Ziel (Name oder DS100)</label>
</div>
<div class="input-field col s12">
diff --git a/templates/api_documentation.html.ep b/templates/api_documentation.html.ep
index e5d026f..9c9ee1f 100644
--- a/templates/api_documentation.html.ep
+++ b/templates/api_documentation.html.ep
@@ -1,10 +1,6 @@
% my $api_root = $self->url_for('/api/v1')->to_abs->scheme('https');
-% my $token = {};
-% my $uid;
-% if (is_user_authenticated()) {
- % $uid = current_user()->{id};
- % $token = get_api_token();
-% }
+% my $token = stash('api_token') // {};
+% my $uid = stash('uid') // q{};
<h1>API</h1>
@@ -31,32 +27,48 @@
<p style="font-family: Monospace;">
{<br/>
"deprecated" : true / false, (falls true: Diese API-Version wird irgendwann abgeschaltet, bitte auf eine neue umsteigen)<br/>
+ "actionTime" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)<br/>
"checkedIn" : true / false,<br/>
+ "comment": "Kommentar",<br/>
"fromStation" : { (letzter Checkin)<br/>
"name" : "Essen Hbf",<br/>
- "ds100" : "EE",<br/>
+ "ds100" : "EE", (ggf. null)<br/>
"uic" : 8000098,<br/>
"latitude" : 51.451355,<br/>
"longitude" : 7.014793,<br/>
"scheduledTime": 1556083680,<br/>
- "realTime": 1556083680,<br/>
+ "realTime": 1556083680<br/>
},<br/>
"toStation" : { (zugehöriger Checkout. Wenn noch nicht eingetragen, sind alle Felder null)<br/>
"name" : "Essen Stadtwald",<br/>
- "ds100" : "EESA",<br/>
+ "ds100" : "EESA", (ggf. null)<br/>
"uic" : 8001896,<br/>
"latitude" : 51.422853,<br/>
"longitude" : 7.023296,<br/>
"scheduledTime": 1556083980, (ggf. null)<br/>
- "realTime": 1556083980, (ggf. null)<br/>
+ "realTime": 1556083980 (ggf. null)<br/>
+ },<br/>
+ "intermediateStops" : [ (Unterwegshalte zwischen fromStation und toStation) <br/>
+ {<br/>
+ "name" : "Essen Süd",<br/>
+ "scheduledArrival" : 1556083800, (ggf. null)<br/>
+ "realArrival" : 1556083800, (ggf. null, nach Ankunft identisch mit scheduledArrival)<br/>
+ "scheduledDeparture" : 1556083860, (ggf. null)<br/>
+ "realDeparture" : 1556083860 (ggf. null, nach Abfahrt identisch mit scheduledDeparture)<br/>
},<br/>
+ …<br/>
+ ],<br/>
"train" : {<br/>
- "type" : "S", (aktueller / letzter Zugtyp)<br/>
- "line" : "6", (Linie als String, nicht immer numerisch, ggf. null)<br/>
- "no" : "30634", (Zugnummer als String)<br/>
- "id" : "7512500863736016593", (IRIS-spezifische Zug-ID)<br/>
+ "type" : "S", (aktueller / letzter Fahrttyp)<br/>
+ "line" : "6", (Linie als String, nicht immer numerisch, ggf. null)<br/>
+ "no" : "30634", (Fahrtnummer als String, ggf. null oder leer)<br/>
+ "id" : "7512500863736016593" (IRIS- oder HAFAS-spezifische Fahrt-ID)<br/>
+ "hafasId" : "1|224479|0|80|30082023" (HAFAS-spezifische Fahrt-ID falls bekannt, ggf. null)<br/>
},<br/>
- "actionTime" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)<br/>
+ "visibility" : {<br/>
+ "desc": "private" / "unlisted" / "followers" / "travelynx" / "public",<br/>
+ "level": 10 / 30 / 60 / 80 / 100<br/>
+ }<br/>
}
</p>
<p>
@@ -65,8 +77,6 @@
</div>
</div>
-% if (app->mode eq 'development') {
-
<h2>Travel</h2>
<div class="row">
<div class="col s12">
@@ -74,26 +84,26 @@
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 des Zugs vorkommt oder nicht.
+ in der vorgesehenen Route der Fahrt vorkommt oder nicht.
</p>
<p>
- Falls du zum Checkinzeitpunkt bereits in einen anderen Zug eingecheckt
+ 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 der vorherige Zug an dieser
+ 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 der
- Zug den angegebenen Zielbahnhof bereits erreicht hat, wird dort
+ 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.
</p>
<p style="font-family: Monospace;">
curl -X POST -H "Content-Type: application/json" -d '{"token":"<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>"}' <%= $api_root %>/travel
</p>
- <p>Payload zum Einchecken, optional mit Zielwahl:</p>
+ <p>Payload zum Einchecken per IRIS-Backend (Schienenverkehr DE/DB), optional mit Zielwahl:</p>
<p style="font-family: Monospace;">
{<br/>
"token" : "<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>",<br/>
@@ -107,6 +117,19 @@
"comment" : "Beliebiger Text" (optional, überschreibt vorherigen Kommentar)<br/>
}
</p>
+ <p>Payload zum Einchecken per HAFAS-Backend (Nahverkehr und außerhalb DE/DB), optional mit Zielwahl. fromStation und toStation müssen mit den Unterwegshalten übereinstimmen, z.B. "Hauptbahnhof (U Gleis 2+4), Essen (Ruhr)" statt "Essen Hbf".</p>
+ <p style="font-family: Monospace;">
+ {<br/>
+ "token" : "<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>",<br/>
+ "action" : "checkin",<br/>
+ "train" : {<br/>
+ "journeyID" : "1|1426396|4|80|19082023",<br/>
+ }<br/>
+ "fromStation" : 651806, (Name oder EVA-Nummer)<br/>
+ "toStation" : 654645, (optional, Name oder EVA-Nummer)<br/>
+ "comment" : "Beliebiger Text" (optional, überschreibt vorherigen Kommentar)<br/>
+ }
+ </p>
<p>Payload zur Wahl eines neuen Ziels, wenn bereits eingecheckt:</p>
<p style="font-family: Monospace;">
{<br/>
@@ -152,7 +175,16 @@
<div class="row">
<div class="col s12">
<p>
- Manueller Import vergangener Zugfahrten (eine Fahrt pro API-Aufruf).
+ Manueller Import vergangener Fahrten (eine Fahrt pro API-Aufruf).
+ </p>
+ <p>
+ Bitte beachten: fromStation, toStation und intermediateStops werden
+ mit Fuzzy Matching eingelesen. Falls ein unbekannter Stationsname
+ einer anderen, bekannten Station hinreichend ähnelt, kann dieser
+ dadurch ersetzt werden. Bei Unsicherheiten empfiehlt sich ein
+ <em>dryRun</em> und ein Vergleich der zurückgegebenen Stationsnamen
+ mit den eingegebenen. Komplett unbekannte Stationsnamen führen
+ standardmäßig zu einem Fehler (siehe <em>lax</em>)
</p>
<p style="font-family: Monospace;">
curl -X POST -H "Content-Type: application/json" -d '{"token":"<%= $uid %>-<%= $token->{import} // 'TOKEN' %>"}' <%= $api_root %>/import
@@ -161,28 +193,26 @@
<p style="font-family: Monospace;">
{<br/>
"token" : "<%= $uid %>-<%= $token->{import} // 'TOKEN' %>",<br/>
- "dryRun" : true/false, (optional: wenn true, wird die Eingabe validiert, aber keine Zugfahrt angelegt)<br/>
- "lax" : true/Fals, (optional: wenn true, werden unbekannte Unterwegshalte akzeptiert)<br/>
- "cancelled" : true/false, (Zugausfall?)<br/>
+ "dryRun" : true/false, (optional: wenn true, wird die Eingabe validiert, aber keine Fahrt angelegt)<br/>
+ "lax" : true/false, (optional: wenn true, werden unbekannte Unterwegshalte akzeptiert)<br/>
+ "cancelled" : true/false, (Ausfall?)<br/>
"train" : {<br/>
- "type" : "S", (Zugtyp, z.B. ICE, RE, S)<br/>
+ "type" : "S", (Typ, z.B. ICE, RE, S, U)<br/>
"line" : "6", (Linie als String, bei Zügen ohne Linie wie IC/ICE u.ä. null)<br/>
- "no" : "30634", (Zugnummer als String)<br/>
+ "no" : "30634", (Nummer als String, ggf. null oder leer)<br/>
},<br/>
"fromStation" : { (Start / Checkin)<br/>
- "name" : "Essen Hbf", (Name oder DS100)<br/>
- "scheduledTime": 1556083680, (UNIX-Timestamp)<br/>
- "realTime": 1556083680, (UNIX-Timestamp, optional, default == scheduledTime)<br/>
+ "name" : "Essen Hbf", (Name oder DS100)<br/>
+ "scheduledTime": 1556083680, (UNIX-Timestamp)<br/>
+ "realTime": 1556083680, (UNIX-Timestamp, optional, default == scheduledTime)<br/>
},<br/>
"toStation" : { (Ziel / Checkout)<br/>
"name" : "Essen Stadtwald", (Name oder DS100)<br/>
"scheduledTime": 1556083980, (UNIX-Timestamp)<br/>
"realTime": 1556083980, (UNIX-Timestamp, optional, default == scheduledTime)<br/>
},<br/>
- "route" : [ (optionale Liste mit Unterwegshalten als Name oder DS100, darf keine Stationen vor Checkin oder nach Checkout beinhalten)<br/>
- "Essen Hbf",<br/>
+ "intermediateStops" : [ (optionale Liste mit Unterwegshalten als Name oder DS100, darf keine Stationen vor Checkin oder nach Checkout beinhalten)<br/>
"Essen Süd",<br/>
- "Essen Stadtwald"<br/>
],<br/>
"comment" : "Beliebiger Text" (optionaler Freitext-Kommentar)<br/>
}
@@ -193,8 +223,10 @@
<p style="font-family: Monospace;">
{<br/>
"success" : true,<br/>
- "id" : 1234, (ID der eingetragenen Zugfahrt)<br/>
- "result" : { ... } (Eingetragene Daten, Datenformat nicht näher spezifiziert und beliebig variabel)<br/>
+ "deprecated" : true / false, (falls true: Diese API-Version wird irgendwann abgeschaltet, bitte auf eine neue umsteigen)<br/>
+ "id" : 1234, (ID der eingetragenen Fahrt)<br/>
+ "result" : { ... } (Eingetragene Daten. Das Datenformat kann sich
+ ohne Berücksichtigung der API-Version ändern)<br/>
}
</p>
<p>
@@ -203,10 +235,9 @@
<p style="font-family: Monospace;">
{<br/>
"success" : false,<br/>
+ "deprecated" : true / false, (falls true: Diese API-Version wird irgendwann abgeschaltet, bitte auf eine neue umsteigen)<br/>
"error" : "Begründung"<br/>
}
</p>
</div>
</div>
-
-% }
diff --git a/templates/bad_request.html.ep b/templates/bad_request.html.ep
new file mode 100644
index 0000000..5d401da
--- /dev/null
+++ b/templates/bad_request.html.ep
@@ -0,0 +1,19 @@
+<div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">400 Bad Request</span>
+ % if (stash('csrf')) {
+ <p>Ungültiger CSRF-Token. Dieser dient zum Schutz vor Cross-Site Request Forgery.</p>
+ <p>Falls du von einer externen Seite hierhin geleitet wurdest, wurde möglicherweise (erfolglos) versucht, deinen Account anzugreifen. Falls du von travelynx selbst aus hier angekommen bist, kann es sich um eine fehlerhafte Cookie-Konfiguration im Browser, eine abgelaufene Session (→ bitte nochmal versuchen) oder du einen Bug in travelynx handeln (→ bitte melden).</p>
+ % }
+ % elsif (my $m = stash('message')) {
+ <p><%= $m %></p>
+ % }
+ % else {
+ <p>Diese Anfrage ist ungültig. Ursache kann z.B. eine abgelaufene Session oder ein Bug in travelynx sein.</p>
+ % }
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/templates/change_name.html.ep b/templates/change_name.html.ep
new file mode 100644
index 0000000..9262734
--- /dev/null
+++ b/templates/change_name.html.ep
@@ -0,0 +1,47 @@
+% if (my $invalid = stash('invalid')) {
+ %= include '_invalid_input', invalid => $invalid
+% }
+
+<h1>Name ändern</h1>
+<div class="row">
+ <div class="col s12">
+ <p>
+ Hier kannst du den Namen deines Accounts ändern. Dieser bestimmt die
+ URL deiner <a href="/status/<%= $name %>">Status</a>- und <a
+ href="/p/<%= $name %>">Profilseite</a> und den Accountnamen beim
+ Anmelden.
+ </p>
+ <p>
+ Beachte, dass der alte Name direkt nach der Umbenennung freigegeben
+ wird und von anderen Accounts belegt werden kann. Für die alten
+ Status- und Profilseiten werden keine Weiterleitungen eingerichtet.
+ </p>
+ </div>
+</div>
+%= form_for '/account/name' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="row">
+ <div class="input-field col s12">
+ <i class="material-icons prefix">lock</i>
+ %= password_field 'password', id => 'password', class => 'validate', required => undef, autocomplete => 'current-password'
+ <label for="password">Aktuelles Passwort</label>
+ </div>
+ <div class="input-field col s12">
+ <i class="material-icons prefix">account_circle</i>
+ %= text_field 'name', id => 'account', class => 'validate', required => undef, pattern => '[0-9a-zA-Z_-]+', maxlength => 60
+ <label for="email">Neuer Name</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="update_name">
+ Ändern
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ <div class="col s3 m3 l3">
+ </div>
+ </div>
+%= end
diff --git a/templates/change_password.html.ep b/templates/change_password.html.ep
index 29aa621..c49226a 100644
--- a/templates/change_password.html.ep
+++ b/templates/change_password.html.ep
@@ -15,12 +15,12 @@
<div class="row">
<div class="input-field col l6 m12 s12">
<i class="material-icons prefix">lock</i>
- %= password_field 'newpw', id => 'password', class => 'validate', required => undef, minlength => 8, autocomplete => 'new-password'
+ %= password_field 'newpw', id => 'password', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password'
<label for="password">Neues Passwort</label>
</div>
<div class="input-field col l6 m12 s12">
<i class="material-icons prefix">lock</i>
- %= password_field 'newpw2', id => 'password2', class => 'validate', required => undef, minlength => 8, autocomplete => 'new-password'
+ %= password_field 'newpw2', id => 'password2', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password'
<label for="password2">Passwort wiederholen</label>
</div>
</div>
diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep
index 7ebc624..09126a8 100644
--- a/templates/changelog.html.ep
+++ b/templates/changelog.html.ep
@@ -2,6 +2,561 @@
<div class="row">
<div class="col s12 m1 l1">
+ 2.6
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Übersichtlichere Darstellung vergangener Fahrten.
+ Patch von Cass Dingenskirchen, vielen Dank!
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Nahverkehr (HAFAS-Backend): Checkins in Fahrten, die mehr als 30
+ Minuten vor/nach dem Anfragezeitpunkt liegen.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.5
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Übersichtlichere Abfahrstafel mit Kennzeichnung der verschiedenen
+ Arten von Verkehrsmitteln. Patch von Cass Dingenskirchen, vielen
+ Dank!
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.4
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Berücksichtigung verwandter Stationen (d.h. Stationen, die zwar
+ gleich heißen, aber intern unterschiedliche IDs haben) bei
+ Checkin-Vorschlägen für Nahverkehrsfahrten. Vorschläge für
+ Zugverbindungen gibt es aus dem Nahverkehrsmenü in vielen Fällen
+ ebenfalls, andersherum meist noch nicht. Die restlichen Feinheiten
+ dieses Themenkomplexes werden im Laufe der Zeit ausgebügelt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.3
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Checkin-Vorschläge für Nahverkehrsfahrten. Die manuelle Angabe von
+ Nahverkehrszielen für Anschlusshinweise entfällt damit. Bei
+ größeren oder aus anderen Gründen im Backend komplexen Stationen
+ werden derzeit teilweise nicht alle möglichen Verbindungen
+ angegeben – dieser Aspekt wird in einem späteren Release
+ verbessert. Eine von der Auswahl von Nah- vs. Fernverkehr
+ unabhängige Liste mit Verbindungsvorschlägen folgt ebenfalls
+ später.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.2
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Hinweis für fehlende Echtzeitdaten (→ nur Fahrplandaten verfügbar)
+ bei der aktuellen Reise.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Korrekte Angabe der Unterwegshalte auch bei fehlenden Echtzeitdaten.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.1
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Vorschlag geeigneter Stationen bei Eingabe eines uneindeutigen
+ Namens auf der Startseite.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Fahrten, die vor Mitternacht begannen, zeigen nun auch nach
+ Mitternacht korrekte Echtzeitdaten an und gehen nicht fälschlich
+ von 24 Stunden Verspätung aus.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Vergangene Fahrten und letzte Fahrtziele werden nun anhand der
+ Abfahrtszeit und nicht anhand der Nummer des Eintrags ausgewählt.
+ Somit können manuelle Einträge für weit in der Vergangenheit
+ liegende Fahrten keine vor kurzem geloggten Fahrten mehr verdecken.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.0
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Checkins in Nahverkehrsmittel (Bus und Bahn) und Züge außerhalb
+ des DB-Netzes per HAFAS-Backend. Die verfügbaren Backends werden
+ per Icon Identifiziert: <i class="material-icons">train</i> IRIS
+ und <i class="material-icons">directions</i> HAFAS.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">announcement</i>
+ Aktuell beschränkt die HAFAS-Anbindung auf Stationssuche, Checkins
+ und Träwelling-Synchronisierung.
+ Eine Einbindung in die Verbindungssuche und das manuelle Nachtragen
+ von HAFAS-Fahrten folgen zu einem späteren Zeitpunkt.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Nicht Rückwärtskompatibel">warning</i>
+ Stationsangaben (z.B. auf der Hauptseite, beim Import oder in der
+ API) müssen nun genau mit der gewünschten Station übereinstimmen.
+ Unbekannte Stationen werden an das HAFAS weitergereicht, welches
+ meist weniger Details bereitstellt als das IRIS.
+ Fuzzy Matching wird nicht mehr in der bisherigen Form unterstützt.
+ Sofern eine Station sowohl via IRIS als auch via HAFAS bekannt ist,
+ wird die IRIS-Version bevorzugt.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">warning</i>
+ Das ds100-Feld in API und Web Hook ist nun optional und bei
+ HAFAS-Checkins null.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.34
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Änderung">build</i>
+ Die Verknüpfung von travelynx zu Träwelling nutzt nun OAuth2
+ anstelle eines passwortbasierten Logins. Einerseits ist OAuth2 eine
+ bedeutend elegantere Lösung; andererseits wird die Träwelling-API
+ für Passwortlogin bald abgeschaltet. Für bestehende
+ Träwelling-Verknüpfungen ergeben sich keine Veränderungen.
+ Neue Verknüpfungen sind weiterhin möglich und benötigen nun keine
+ Angabe von E-Mail und Passwort mehr. Selbst
+ gehostete travelynx-Instanzen, die die Träwelling-Verknüpfung
+ anbieten möchten, müssen ab soforn bei Träwelling eine eigene <a
+ href="https://traewelling.de/settings/applications">Anwendung
+ anlegen</a> und in travelynx konfigurieren. Bitte auch die neue
+ Dependency Mojolicious::Plugin::OAuth2 im cpanfile beachten.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">announcement</i>
+ Derzeit unterstützt travelynx neben Bahnhofsnamen auch EVA-IDs und
+ DS100/Ril100-Codes. In Zukunft werden in einzelnen Fällen nur noch
+ Bahnhofsnamen und EVA-IDs unterstützt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.33
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Synchronisierung der Checkin-Sichtbarkeit von travelynx zu
+ Träwelling (Patch von networkException).
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ <a href="/timeline/in-transit">Timeline-Ansicht</a> mit aktuellen
+ Checkins gefolgter Accounts. Die Timeline wird von der Homepage
+ verlinkt, wenn passende Checkins vorliegen.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Angabe von passenden Checkins gefolgter Accounts in der
+ Abfahrtstafel (im Sinne von: „der folgende Account ist mit auch mit
+ diesem Zug unterwegs“).
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.32
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Angabe von Kommentaren und Sichtbarkeit in der JSON-API
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Editierbare Beschreibung und optionale Links auf der Profilseite.
+ Hier können beispielsweise Träwelling oder andere Social
+ Media-Accounts eingetragen werden.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Optional: folgen von Accounts. Die Sichtbarkeit von Checkins und vergangenen
+ Fahrten kann somit auf Follower eingeschränkt werden. Eine
+ Übersichtsseite mit aktuellen Checkins gefolgter Accounts (ähnlich
+ zur Timeline im Fediverse) folgt in einem späteren Release.<br/>
+ Für jeden Account kann individuell eingestellt werden, ob Accounts
+ ihm folgen können, ob Folge-Anfragen zunächst angenommen werden
+ müssen oder ob Folgen grundszätzlich nicht möglich ist.
+ Standardmäßig ist dieses Feature inaktiv: Folge(anfrage)n müssen
+ zunächst in den Einstellung aktiviert werden. Falls notwendig,
+ können einzelne Accounts blockiert und dadurch am Folgen
+ und am Stellen von Folge-Anfragen gehindert werden.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.31
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Vorhalten der Echtzeitdaten von Unterwegshalten.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.30
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Individuelle Sichtbarkeit für jede Fahrt. Optional können Fahrten
+ und Check-Ins nur mit einem explizit geteilten Link für andere
+ Personen sichtbar sein.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.29
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Jahresrückblick mit erweiterten Statistiken.
+ Der Rückblick ist jeweils ab dem 31.12. verfügbar.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.28
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Behandlung von nicht mehr im IRIS eingepflegten Stationen bei vergangenen Reisen.
+ Bislang hatten diese zu unvollständigen Reisestatistiken geführt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.27
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Angabe von „Kein Zustieg“ (Abfahrtstafel) bzw. „Kein Ausstieg“ (Route) durch eingeklammerte Uhrzeiten „(HH:MM)“.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.26
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Angabe der erwarteten Zugauslastung bei Unterwegshalten und Anschlussvorschlägen, sofern verfügbar.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.25
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Interne Änderungen">star</i>
+ Umstellung der Träwelling-Anbindung auf Träwelling-API v1, da v0
+ sukzessive abgeschaltet wird. API v1 ist noch nicht stabil.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Interne Änderungen">star</i>
+ Nutzung eines internen HAFAS-mgate.exe-Clients anstelle von transport.rest.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.24
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Angabe der geschätzten Ankunft am Ziel bei Checkinvorschlägen.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Anzeige von Anschlussmöglichkeiten an den Nahverkehr (Bus und
+ Stadtbahn) unterhalb der Anschlusszüge. Da travelynx derzeit keine
+ Checkins in Nahverkehrsmittel unterstützt, muss die Liste relevanter
+ Ziele händisch unter Account → Verbindungen gepflegt werden. Sofern
+ eine zukünftige travelynx-Version Nahverkehrs-Checkins unterstützt,
+ entfällt diese Liste.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Optionale Verlinkung externer Dienste (z.B. DBF oder bahn.expert)
+ in der eigenen Checkin-Ansicht. Somit können von dort aus alle
+ Abfahrten an einer Ziel- oder Unterwegsstation eingesehen werden.
+ Dieses Feature ist standardmäßig deaktiviert und kann über
+ Account → Externe Dienste konfiguriert werden.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Checkinvorschläge für Anschlussverbindungen schauen weiter in die
+ Zukunft und enthalten weniger nutzlose Vorschläge (z.B. Rückfahrt zur
+ Ursprungsstation oder Weiterfahrt zu einem späteren Ziel mit dem
+ Folgetakt).
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.23
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Optionale Links zu externen Abfahrtsmonitoren in der Halteliste des
+ aktuell ausgewählten Zugs. Die Abfahrtstafelseite kann bei den
+ Account-Einstellungen konfiguriert werden.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.22
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Verbesserte Verknüpfung und Synchronisierung mit
+ <a href="https://traewelling.de">Träwelling</a>.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Inaktive Accounts erhalten nach einem Jahr eine E-Mail, die auf die
+ in vier Wochen folgende Löschung hinweist. Betreiber:innen einer
+ selbstgehosteten travelynx-Instanz müssen hierzu <i>base_url</i>
+ in travelynx.conf setzen (siehe examples/travelynx.conf).
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.21
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Annotation von Fahrten mit fehlenden Echtzeitdaten in der
+ Abfahrtstafel. Derzeit wird das Vorhandensein von Echtzeitdaten
+ noch nicht im Fahrtenbuch gespeichert.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Unterstützung von Maßnahmen zum Schutz vor E-Mail-Spam über das
+ Registrierungsformular.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.20
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Accountnamen können jetzt in den Einstellungen geändert werden.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.19
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Hinweis bei knapper Umstiegszeit zu Anschlussverbindungen.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Übersichtlichere Navigation in der Fahrtenliste / Statistikansicht.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.18
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Autmatische Checkin-Synchronisierung mit
+ <a href="https://traewelling.de">Träwelling</a>. Checkins können
+ entweder von Träwelling zu travelynx oder von travelynx zu Träwelling
+ übernommen werden. Das Feature läuft vorerst als Public Beta.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.17
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Öffentliches Profil. Hier können auf Wunsch der aktuelle Status
+ sowie die letzten zehn Zugfahrten angezeigt werden. Diese sind
+ wahlweise gar nicht, nur mit Anmeldung oder öffentlich sichtbar.
+ Zugfahrten, die älter als vier Wochen sind, können komplett verborgen
+ werden.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.16
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Auswertung von Zugfahrten von/nach bestimmten Stationen
+ aufgeschlüsselt nach Jahr und Monat. Diese Daten können für die
+ Angaben zur Pendlerpauschale bei der Steuererklärung nützlich sein.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ CSV-Export aller Fahrten.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.15
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i> Die
+ über „Teilen“ verfügbare Reisestatus-Seite kann nun auch Details
+ beendeter Zugfahrten anzeigen, wenn die entsprechende Option in den <a
+ href="/account/privacy">Privatsphäre-Einstellungen</a> aktiv ist.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Behandlung von Haltausfällen während der Reise bzw. nach dem Checkin.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.14
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left">add</i> API-Endpunkt zum Import
+ nicht in travelynx getrackter Zugfahrten.
+ </p>
+ <p>
+ <i class="material-icons left">add</i> Status-API: Angabe der
+ Unterwegshalte zwischen Start und Ziel.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.13
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left">add</i> API-Endpunkt zum Einchecken in
+ Züge, aktualisieren der Zielwahl und Zurücknehmen des letzten
+ Checkins.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 1.12
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left">add</i> Unterstützung der im
+ Fahrplanjahr 2020 hinzugekommenen und geänderten Stationen. Start
+ und Ziel von mit travelynx geloggten Fahrten sind nun vom
+ Stationsnamen unabhängig (d.h. Namensänderungen werden automatisch
+ übernommen).
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
1.11
</div>
<div class="col s12 m11 l11">
@@ -167,5 +722,3 @@
</ul>
</div>
</div>
-
-%= include '_footer', version => stash('version')
diff --git a/templates/commute.html.ep b/templates/commute.html.ep
new file mode 100644
index 0000000..26b2fbc
--- /dev/null
+++ b/templates/commute.html.ep
@@ -0,0 +1,91 @@
+<div class="row">
+ <div class="col s12">
+ <p>
+ Hier werden nur Fahrten angezeigt, deren Start- oder Zielstation
+ den angegebenen Kriterien entpricht. Diese Daten können zum Beispiel für
+ die Angaben zur Pendlerpauschale bei der Steuererklärung genutzt
+ werden.
+ </p>
+ </div>
+</div>
+
+%= form_for '/history/commute' => begin
+ <div class="row">
+ <div class="input-field col s12 m12 l12">
+ %= text_field 'year', id => 'year', class => 'validate', pattern => '[0-9][0-9][0-9][0-9]'
+ <label for="year">Jahr</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12 m12 l6">
+ <div>
+ <label>
+ %= radio_button filter_type => 'exact'
+ <span>Name der Station ist:</span>
+ </label>
+ </div>
+ <div>
+ <label>
+ %= radio_button filter_type => 'substring'
+ <span>Name der Station enthält:</span>
+ </label>
+ </div>
+ <div>
+ <label>
+ %= radio_button filter_type => 'regex'
+ <span>Name der Station erfüllt den regulären Ausdruck:</span>
+ </label>
+ </div>
+ </div>
+ <div class="input-field col s12 m12 l6">
+ %= text_field 'station', id => 'station', required => undef, class => 'autocomplete contrast-color-text', autocomplete => 'off'
+ <label for="station">Fahrtziel</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12 m12 l12 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="show">
+ <i class="material-icons left" aria-hidden="true">send</i>
+ Anzeigen
+ </button>
+ </div>
+ </div>
+%= end
+
+<h1><%= param('year') %></h1>
+<div class="row">
+ <div class="col s12 m12 l12">
+ <p>
+ An <b><%= $total_journeys %></b> Tagen im Jahr wurde mindestens
+ eine Fahrt von oder zu
+ % if (param('filter_type') eq 'exact') {
+ der ausgewählten Station
+ % }
+ % else {
+ den ausgewählten Stationen
+ % }
+ eingetragen.
+ </p>
+ <table class="striped">
+ <thead>
+ <tr>
+ <th>Monat</th>
+ <th>Tage mit Fahrten</th>
+ </tr>
+ </thead>
+ <tbody>
+ % for my $i (0 .. $#{$months}) {
+ <tr>
+ <td><%= $months->[$i] %></td>
+ <td><%= $count_by_month->{$i+1} // 0 %></td>
+ </tr>
+ % }
+ </tbody>
+ </table>
+ </div>
+</div>
+
+% for my $i (0 .. $#{$months}) {
+ <h2><%= $months->[$i] %></h2>
+ %= include '_history_trains', date_format => '%a %d.%m.', journeys => $journeys_by_month->{$i+1} // []
+% }
diff --git a/templates/departures.html.ep b/templates/departures.html.ep
index e71fa25..6aac482 100644
--- a/templates/departures.html.ep
+++ b/templates/departures.html.ep
@@ -1,103 +1,149 @@
<div class="row">
- <div class="col s12 center-align">
- <b><%= $station %></b>
+ <div class="col s12">
+ <h2>
+ <i class="material-icons " aria-hidden="true"><%= param('hafas') ? 'directions' : 'train' %></i>
+ <%= $station %>
+ </h2>
% for my $related_station (sort { $a->{name} cmp $b->{name} } @{$related_stations}) {
- <br/><%= $related_station->{name} %>
+ + <%= $related_station->{name} %> <br/>
% }
</div>
</div>
-% my $status = $self->get_user_status;
+% if ($api_link) {
+<div class="row">
+ <div class="col s12 center-align">
+ % if (param('hafas')) {
+ <a href="<%= $api_link %>" class="btn-small"><i class="material-icons left" aria-hidden="true">train</i>zum Schienenverkehr</a>
+ % }
+ % else {
+ <a href="<%= $api_link %>" class="btn-small"><i class="material-icons left" aria-hidden="true">directions</i>zum Nahverkehr</a>
+ % }
+ </div>
+</div>
+% }
+
% my $have_connections = 0;
-% if ($status->{checked_in}) {
+% if ($user_status->{checked_in}) {
<div class="row">
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title">Aktuell eingecheckt</span>
- <p>In <%= $status->{train_type} %> <%= $status->{train_no} %>
- ab <%= $status->{dep_name} %></p>
+ <p>In <%= $user_status->{train_type} %> <%= $user_status->{train_no} %>
+ % if ( $user_status->{arr_name}) {
+ von <%= $user_status->{dep_name} %> nach <%= $user_status->{arr_name} %>
+ % }
+ % else {
+ ab <%= $user_status->{dep_name} %>
+ % }
+ </p>
</div>
<div class="card-action">
- <a class="action-checkout" data-station="<%= $ds100 %>" data-force="1">
- Hier auschecken
- </a>
+ % if ($can_check_out) {
+ <a class="action-undo" data-id="in_transit" data-checkints="<%= $user_status->{timestamp}->epoch %>" style="margin-right: 0;">
+ <i class="material-icons left" aria-hidden="true">undo</i> Rückgängig
+ </a>
+ <a class="action-checkout right" data-station="<%= $eva %>" data-force="1">
+ Hier auschecken
+ </a>
+ % }
+ % else {
+ <a class="action-undo" data-id="in_transit" data-checkints="<%= $user_status->{timestamp}->epoch %>" style="margin-right: 0;">
+ <i class="material-icons left" aria-hidden="true">undo</i> Rückgängig
+ </a>
+ <a class="action-checkout right" data-station="<%= $eva %>" data-force="1">
+ <i class="material-icons left" aria-hidden="true">gps_off</i>
+ Hier auschecken
+ </a>
+ % }
</div>
</div>
</div>
</div>
% }
-% elsif ($status->{timestamp_delta} < 180) {
+% elsif ($user_status->{cancellation} and $station eq $user_status->{cancellation}{dep_name}) {
<div class="row">
<div class="col s12">
- %= include '_checked_out', journey => $status;
+ %= include '_cancelled_departure', journey => $user_status->{cancellation};
</div>
</div>
% }
-% elsif (not param('train') and my @connections = get_connecting_trains(ds100 => $ds100)) {
+% 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') // []}) ) {
% $have_connections = 1;
<div class="row">
<div class="col s12">
- <p>Häufig genutzte Verbindungen – Zug auswählen zum Einchecken mit Zielwahl</p>
- %= include '_connections', connections => \@connections, checkin_from => $ds100;
+ <p>Häufig genutzte Verbindungen – Fahrt auswählen zum Einchecken mit Zielwahl</p>
+ % if (@{stash('connections_iris') // []}) {
+ %= include '_connections', connections => stash('connections_iris'), checkin_from => $eva;
+ % }
+ % if (@{stash('connections_hafas') // []}) {
+ %= include '_connections_hafas', connections => stash('connections_hafas'), checkin_from => $eva;
+ % }
</div>
</div>
% }
+
+<div class="row">
+ <div class="col s4 center-align">
+ % if ($hafas) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({hafas => 1, timestamp => $datetime->clone->subtract(hours => 1)->epoch}) %>"><i class="material-icons left" aria-hidden="true">chevron_left</i><span class="hide-on-small-only">früher</span></a>
+ % }
+ </div>
+ <div class="col s4 center-align">
+ % if ($now_in_range) {
+ <a class="btn-small" href="#now"><i class="material-icons left" aria-hidden="true">vertical_align_center</i><span class="hide-on-small-only">Jetzt</span></a>
+ % }
+ </div>
+ <div class="col s4 center-align">
+ % if ($hafas) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({hafas => 1, timestamp => $datetime->clone->add(hours => 1)->epoch}) %>"><span class="hide-on-small-only">später</span><i class="material-icons right" aria-hidden="true">chevron_right</i></a>
+ % }
+ </div>
+</div>
+
<div class="row">
<div class="col s12">
<p>
% if ($have_connections) {
Alle Abfahrten –
% }
- % if (@{$results}) {
- Zug auswählen zum Einchecken.
+ % if ($user_status->{checked_in} and not $can_check_out) {
+ Diese Station liegt nicht auf der Route deines <a href="/">aktuellen Checkins</a>.
+ Falls du aktuell nicht mit <b><%= $user_status->{train_type} %> <%= $user_status->{train_no} %></b> unterwegs bist, kannst du den Checkin rückgängig machen.
+ Falls es sich bei <b><%= $station %></b> um einen nicht in den Echtzeitdaten abgebildeten Zusatzhalt handelt, kannst du hier auchecken.
+ Da travelynx nicht weiß, welcher der beiden Fälle zutrifft, sind bis dahin keine neuen Checkins möglich.
+ % }
+ % elsif ($user_status->{checked_in} and not $user_status->{arr_eva}) {
+ Du bist bereits eingecheckt und hast noch kein Fahrtziel angegeben.
+ Bitte <a href="/">wähle zunächst ein Ziel</a>.
+ Neue Checkins sind erst nach Ankunft der aktuellen Fahrt möglich.
+ % }
+ % elsif ($user_status->{checked_in} and $user_status->{arrival_countdown} > 0) {
+ Deine aktuelle Fahrt ist <a href="/">noch unterwegs</a>.
+ Ein neuer Checkin ist erst nach Ankunft am ausgewählten Ziel möglich.
+ % }
+ % elsif (@{$results}) {
+ Fahrt auswählen zum Einchecken.
% }
% else {
Keine Abfahrten gefunden. Ein Checkin ist frühestens 30 Minuten vor
und maximal 120 Minuten nach Abfahrt möglich.
% }
</p>
- <table class="striped">
- <tbody>
- % my $orientation_bar_shown = param('train');
- % my $now_epoch = now()->epoch;
- % for my $result (@{$results}) {
- % my $td_class = '';
- % my $link_class = 'action-checkin';
- % if ($result->departure_is_cancelled) {
- % $td_class = "cancelled";
- % $link_class = 'action-cancelled-from';
- % }
- % if (not $orientation_bar_shown and $result->departure->epoch < $now_epoch) {
- % $orientation_bar_shown = 1;
- <tr>
- <td>
- </td>
- <td>
- — Anfragezeitpunkt —
- </td>
- <td>
- </td>
- </tr>
- % }
- <tr>
- <td>
- <a class="<%= $link_class %>" data-station="<%= $result->station_uic %>" data-train="<%= $result->train_id %>">
- <%= $result->line %>
- </a>
- </td>
- <td class="<%= $td_class %>">
- <a class="<%= $link_class %>" data-station="<%= $result->station_uic %>" data-train="<%= $result->train_id %>">
- <%= $result->destination %>
- </a>
- </td>
- <td class="<%= $td_class %>"><%= $result->departure->strftime('%H:%M') %>
- % if ($result->departure_delay) {
- (<%= sprintf('%+d', $result->departure_delay) %>)
- % }
- </td>
- </tr>
- % }
- </tbody>
- </table>
+ % if (not $user_status->{checked_in} or ($can_check_out and $user_status->{arr_eva} and $user_status->{arrival_countdown} <= 0)) {
+ % if ($hafas) {
+ %= include '_departures_hafas', results => $results;
+ % }
+ % else {
+ %= include '_departures_iris', results => $results;
+ % }
+ % }
</div>
</div>
diff --git a/templates/disambiguation.html.ep b/templates/disambiguation.html.ep
new file mode 100644
index 0000000..270aa99
--- /dev/null
+++ b/templates/disambiguation.html.ep
@@ -0,0 +1,20 @@
+<div class="row">
+ <div class="col s12">
+ <div class="card info-color">
+ <div class="card-content">
+ <span class="card-title">Mehrdeutige Eingabe</span>
+ <p>„<%= $station %>“ ist nicht eindeutig. Bitte wähle eine der folgenden Optionen aus.</p>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12">
+ <ul class="suggestions">
+ % for my $suggestion (@{$suggestions // []}) {
+ <li><a href="<%= url_for('station' => $suggestion->{eva}) . (param('hafas') ? '?hafas=1' : q{}) %>"><%= $suggestion->{name} %></a></li>
+ % }
+ </ul>
+ </div>
+</div>
diff --git a/templates/edit_comment.html.ep b/templates/edit_comment.html.ep
index 81353a2..80c4110 100644
--- a/templates/edit_comment.html.ep
+++ b/templates/edit_comment.html.ep
@@ -1,11 +1,11 @@
-<h1>Zugfahrt kommentieren</h1>
+<h1>Fahrt kommentieren</h1>
% if ($error or not $journey->{checked_in}) {
<div class="row">
<div class="col s12">
<div class="card caution-color">
<div class="card-content white-text">
<span class="card-title">Fehler</span>
- <p>Du bist gerade nicht eingecheckt. Vergangene Zugfahrten
+ <p>Du bist gerade nicht eingecheckt. Vergangene Fahrten
kannst du über die Editierfunktion in der History
kommentieren.</p>
</div>
@@ -29,7 +29,7 @@
am
<b><%= $journey->{sched_departure}->strftime('%d.%m.%Y') %></b>
</p>
- % if (current_user()->{is_public} & 0x04) {
+ % if (current_user()->{comments_visible}) {
<p>
Der hier eingetragene Text ist als Teil deines Nutzerstatus
öffentlich sichtbar.
diff --git a/templates/edit_journey.html.ep b/templates/edit_journey.html.ep
index ff36381..cb867e5 100644
--- a/templates/edit_journey.html.ep
+++ b/templates/edit_journey.html.ep
@@ -1,11 +1,11 @@
-<h1>Zugfahrt bearbeiten</h1>
+<h1>Fahrt bearbeiten</h1>
% if ($error and $error eq 'notfound') {
<div class="row">
<div class="col s12">
<div class="card caution-color">
<div class="card-content white-text">
<span class="card-title">Fehler</span>
- <p>Zugfahrt nicht gefunden.</p>
+ <p>Fahrt nicht gefunden.</p>
</div>
</div>
</div>
@@ -39,11 +39,11 @@
</p>
<p>
Nach einer Änderung können die ursprünglich eingetragenen
- Zeiten nicht mehr wiederhergestellt werden.
+ Daten nicht wiederhergestellt werden.
</p>
<table class="striped">
<tr>
- <th scope="row">Zug</th>
+ <th scope="row">Fahrt</th>
<td>
<%= $journey->{type} %> <%= $journey->{no} %>
% if ($journey->{line}) {
@@ -61,38 +61,50 @@
</td>
</tr>
<tr>
+ <th scope="row">Start:</th>
+ <td class="input-field">
+ %= text_field 'from_name', id => 'from_name', class => 'autocomplete validate', autocomplete => 'off', required => undef
+ </td>
+ </tr>
+ <tr>
<th scope="row">Geplante Abfahrt</th>
- <td>
+ <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]'
</td>
</tr>
<tr>
<th scope="row">Tatsächliche Abfahrt</th>
- <td>
+ <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]'
</td>
</tr>
<tr>
+ <th scope="row">Ziel:</th>
+ <td class="input-field">
+ %= text_field 'to_name', id => 'to_name', class => 'autocomplete validate', autocomplete => 'off', required => undef
+ </td>
+ </tr>
+ <tr>
<th scope="row">Geplante Ankunft</th>
- <td>
+ <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]'
</td>
</tr>
<tr>
<th scope="row">Tatsächliche Ankunft</th>
- <td>
+ <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]'
</td>
</tr>
<tr>
<th scope="row">Route</th>
- <td>
+ <td class="input-field">
%= text_area 'route', id => 'route', class => 'materialize-textarea'
</td>
</tr>
<tr>
<th scope="row">Kommentar</th>
- <td>
+ <td class="input-field">
%= text_field 'comment'
</td>
</tr>
@@ -108,7 +120,7 @@
<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>
+ <i class="material-icons right" aria-hidden="true">send</i>
</button>
</div>
</div>
diff --git a/templates/edit_profile.html.ep b/templates/edit_profile.html.ep
new file mode 100644
index 0000000..55b1e1e
--- /dev/null
+++ b/templates/edit_profile.html.ep
@@ -0,0 +1,60 @@
+<div class="row">
+ <div class="col s12">
+ <h1>Profil bearbeiten</h1>
+ </div>
+</div>
+%= form_for '/account/profile' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="row">
+ <div class="col s12">
+ <div class="card">
+ <div class="card-content">
+ <span class="card-title"><%= $name %></span>
+ <p>
+ Markdown möglich, maximal 2000 Zeichen.
+ %= text_area 'bio', id => 'bio', class => 'materialize-textarea'
+ </p>
+ </div>
+ <div class="card-action">
+ <a href="/p/<%= $name %>" class="waves-effect waves-light btn">
+ Abbrechen
+ </a>
+ <button class="btn waves-effect waves-light right" type="submit" name="action" value="save">
+ Speichern
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12">
+ Metadaten: Markdown-Links im Inhalt erlaubt, jeweils maximal 500 Zeichen
+ </div>
+ </div>
+ % for my $i (0 .. 10) {
+ <div class="row">
+ <div class="input-field col l3 m12 s12">
+ %= text_field "key_$i", id => "key_$i", maxlength => 50
+ <label for="key_<%= $i %>">Attribut</label>
+ </div>
+ <div class="input-field col l9 m12 s12">
+ %= text_field "value_$i", id => "value_$i", maxlength => 500
+ <label for="value_<%= $i %>">Inhalt</label>
+ </div>
+ </div>
+ % }
+ <div class="row center-align">
+ <div class="col s6">
+ <a href="/p/<%= $name %>" class="waves-effect waves-light btn">
+ Abbrechen
+ </a>
+ </div>
+ <div class="col s6">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
+ Speichern
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ </div>
+%= end
diff --git a/templates/edit_visibility.html.ep b/templates/edit_visibility.html.ep
new file mode 100644
index 0000000..9bf8d56
--- /dev/null
+++ b/templates/edit_visibility.html.ep
@@ -0,0 +1,123 @@
+<h1>Sichtbarkeit ändern</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">Fehler</span>
+ <p><%= $error // 'Du bist gerade nicht eingecheckt' %></p>
+ </div>
+ </div>
+ </div>
+ </div>
+% }
+% else {
+ %= form_for '/journey/visibility' => (method => 'POST') => begin
+ %= csrf_field
+ %= hidden_field 'dep_ts' => param('dep_ts')
+ %= hidden_field 'id' => param('id')
+ <div class="row">
+ <div class="col s12">
+ <p>
+ Fahrt mit
+ <b><%= $journey->{train_type} // $journey->{type} %> <%= $journey->{train_no} // $journey->{no} %></b>
+ von
+ <b><%= $journey->{dep_name} // $journey->{from_name} %></b>
+ nach
+ <b><%= $journey->{arr_name} // $journey->{to_name} // 'irgendwo' %></b>
+ am
+ <b><%= $journey->{sched_departure}->strftime('%d.%m.%Y') %></b>
+ </p>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'default'
+ <span>Einstellung aus dem Profil verwenden: <strong>
+ % if ($user_level eq 'public') {
+ Die Fahrt ist öffentlich sichtbar.
+ % }
+ % elsif ($user_level eq 'travelynx') {
+ Die Fahrt ist nur für auf dieser Seite angemeldete Accounts oder mit Link sichtbar.
+ % }
+ % elsif ($user_level eq 'followers') {
+ Die Fahrt ist nur für dir folgende Accounts oder mit Link sichtbar.
+ % }
+ % elsif ($user_level eq 'unlisted') {
+ Die Fahrt ist nur mit Link sichtbar.
+ % }
+ % else {
+ Die Fahrt ist nur für dich sichtbar.
+ % }
+ </strong> Änderungen der Profil-Einstellung werden auch nachträglich für diese Fahrt wirksam.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'public'
+ <span><i class="material-icons left"><%= visibility_icon('public') %></i>Öffentlich: Im Profil verlinkt und beliebig zugänglich.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'travelynx'
+ <span><i class="material-icons left"><%= visibility_icon('travelynx') %></i>Intern: Personen, die dir folgen, die auf dieser Seite angemeldet sind oder denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'followers'
+ <span><i class="material-icons left"><%= visibility_icon('followers') %></i>Follower: Personen, die dir folgen oder denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'unlisted'
+ <span><i class="material-icons left"><%= visibility_icon('unlisted') %></i>Verlinkbar: Personen, denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'private'
+ <span><i class="material-icons left"><%= visibility_icon('private') %></i>Privat: nur für dich sichtbar.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s6 m6 l6 center-align">
+ <a href="/" class="waves-effect waves-light btn">
+ Abbrechen
+ </a>
+ </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>
+ %= end
+% }
diff --git a/templates/exception.html.ep b/templates/exception.html.ep
index 290efc5..ec01ad2 100644
--- a/templates/exception.html.ep
+++ b/templates/exception.html.ep
@@ -21,7 +21,7 @@
%= DateTime->now(time_zone => 'Europe/Berlin')->strftime("%d/%b/%Y:%H:%M:%S %z")
<br/><br/>
Message:
- %= (split(qr{\n}, $exception->message))[0]
+ %= ref($exception) ? (split(qr{\n}, $exception->message))[0] : $exception
</p>
</div>
</div>
diff --git a/templates/history.html.ep b/templates/history.html.ep
index 93fa9f7..71d180f 100644
--- a/templates/history.html.ep
+++ b/templates/history.html.ep
@@ -1,58 +1,46 @@
-<h1>Fahrten</h1>
+<h2>Fahrten</h2>
-<div class="row">
- <div class="col s12">
- Hier finden sich alle bisherigen Zugfahrten und Statistiken für jedes
- Jahr und jeden Monat.
- </div>
-</div>
+Für Details ein Jahr auswählen.
-<h2>Nach Jahr</h2>
-%= include '_history_years', current => '';
-% if(0) {
- <div class="row">
- <div class="col s12">
- Noch keine Fahrten.
- </div>
- </div>
-% }
-
-<h2>Nach Monat</h2>
-%= include '_history_months', current => '';
-% if(0) {
- <div class="row">
- <div class="col s12">
- Noch keine Fahrten.
- </div>
- </div>
-% }
+%= include '_history_years_list';
<h2>Auswertungen</h2>
<div class="row">
- <div class="col s12 m12 l12 center-align">
- <a href="/history/map" class="waves-effect waves-light btn"><i class="material-icons left">map</i> Fahrtenkarte</a>
+ <div class="col s12 m12 l5 center-align">
+ <a href="/history/map" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Fahrtenkarte</a>
+ </div>
+ <div class="col s12 m12 l2">&nbsp;</div>
+ <div class="col s12 m12 l5 center-align">
+ <a href="/history/commute" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">search</i> nach Station</a>
</div>
</div>
<h2>Ausfälle und Verspätungen</h2>
<div class="row">
<div class="col s12 m12 l5 center-align">
- <a href="/cancelled" class="waves-effect waves-light btn"><i class="material-icons left">cancel</i> Zugausfälle</a>
+ <a href="/cancelled" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">cancel</i> Ausfälle</a>
</div>
<div class="col s12 m12 l2">&nbsp;</div>
<div class="col s12 m12 l5 center-align">
- <a href="/fgr" class="waves-effect waves-light btn"><i class="material-icons left">feedback</i> Fahrgastrechte</a>
+ <a href="/fgr" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">feedback</i> Fahrgastrechte</a>
</div>
</div>
<h2>Rohdaten</h2>
<div class="row">
<div class="col s12 m12 l5 center-align">
- <a href="/history.json" class="waves-effect waves-light btn"><i class="material-icons left">cloud</i> JSON-Export</a>
+ <a href="/history.json" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">cloud</i> JSON-Export</a>
+ </div>
+ <div class="col s12 m12 l2">&nbsp;</div>
+ <div class="col s12 m12 l5 center-align">
+ <a href="/history.csv" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">list</i> CSV-Export</a>
</div>
+</div>
+<div class="row">
+ <div class="col s12 m12 l5 center-align">&nbsp;</div>
<div class="col s12 m12 l2">&nbsp;</div>
<div class="col s12 m12 l5 center-align">
- <a href="/journey/add" class="waves-effect waves-light btn"><i class="material-icons left">add</i> Neue Fahrt</a>
+ <a href="/journey/add" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">add</i> Neue Fahrt</a>
</div>
</div>
diff --git a/templates/history_by_month.html.ep b/templates/history_by_month.html.ep
index 7aae59d..9ad7031 100644
--- a/templates/history_by_month.html.ep
+++ b/templates/history_by_month.html.ep
@@ -1,6 +1,4 @@
-%= include '_history_months', current => "${year}/${month}";
-
-<h1><%= stash('month_name') %> <%= stash('year') %></h1>
+%= include '_history_months';
% if (stash('statistics')) {
%= include '_history_stats', stats => stash('statistics');
diff --git a/templates/history_by_year.html.ep b/templates/history_by_year.html.ep
index c5cffa0..6aa0c2d 100644
--- a/templates/history_by_year.html.ep
+++ b/templates/history_by_year.html.ep
@@ -1,11 +1,36 @@
%= include '_history_years', current => $year;
-<h1>Jahresrückblick <%= $year %></h1>
-
% if (stash('statistics')) {
%= include '_history_stats', stats => stash('statistics');
% }
+<div class="row">
+ % if (stash('have_review')) {
+ <div class="col s12 m12 l5 center-align">
+ <a href="/history/map?filter_from=1.1.<%= $year %>&amp;filter_to=31.12.<%= $year %>" 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">&nbsp;</div>
+ <div class="col s12 m12 l5 center-align">
+ <a href="/history/<%= $year %>/review" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">camera_roll</i> Rückblick</a>
+ </div>
+ % }
+ % else {
+ <div class="col s12 m12 l12 center-align">
+ <a href="/history/map?filter_from=1.1.<%= $year %>&amp;filter_to=31.12.<%= $year %>" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a>
+ </div>
+ % }
+</div>
+
+%= include '_history_months_for_year';
+
+% if (param('filter') and param('filter') eq 'single') {
+<div class="row">
+ <div class="col s12 m12 l12">
+ <p>Die folgende Auflistung enthält nur Fahrten, deren Kombination aus Start und Ziel im aktuellen Jahr einmalig ist.</p>
+ </div>
+</div>
+% }
+
% if (stash('journeys')) {
%= include '_history_trains', date_format => '%d.%m.', journeys => stash('journeys');
% }
diff --git a/templates/history_map.html.ep b/templates/history_map.html.ep
index 7bda06b..57ba81f 100644
--- a/templates/history_map.html.ep
+++ b/templates/history_map.html.ep
@@ -1,31 +1,141 @@
<div class="row">
<div class="col s12">
% if (@{$station_coordinates}) {
- Alle bisherigen Zugfahrten
+ Fahrten
% }
% else {
- Keine Zugfahrten gefunden.
+ Keine Fahrten
+ % }
+ % if (param('filter_type')) {
+ mit <strong><%= param('filter_type') %></strong>
+ % }
+ % if (stash('year')) {
+ im Jahr <strong><%= stash('year') %></strong>
+ % }
+ % elsif (param('filter_from') and param('filter_to')) {
+ zwischen dem <strong><%= param('filter_from') %></strong> und dem <strong><%= param('filter_to') %></strong>
+ % }
+ % elsif (param('filter_from')) {
+ ab dem <strong><%= param('filter_from') %></strong>
+ % }
+ % elsif (param('filter_to')) {
+ bis einschließlich <strong><%= param('filter_to') %></strong>
+ % }
+ % elsif (@{$station_coordinates}) {
+ in travelynx
+ % }
+ % if (not @{$station_coordinates}) {
+ gefunden
% }
</div>
</div>
-%= include '_map', station_coordinates => $station_coordinates, station_pairs => $station_pairs
+%= include '_map', station_coordinates => $station_coordinates, polyline_groups => $polyline_groups
-%= form_for '/history/map' => (method => 'POST') => begin
- %= csrf_field
+%= form_for '/history/map' => begin
+ <p>
+ Detailgrad:
+ </p>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button route_type => 'polyline'
+ <span>Nur Fahrten mit bekanntem Streckenverlauf eintragen</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button route_type => 'polybee'
+ <span>Streckenverlauf wenn bekannt, sonst Luftlinie zwischen Unterweghalten</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button route_type => 'beeline'
+ <span>Immer Luftlinie zwischen Unterwegshalten zeigen</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= check_box include_manual => 1
+ <span>Manuelle Einträge ohne Unterwegshalte mitberücksichtigen</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12 center-align">
+ <button class="btn wave-effect waves-light" type="submit">
+ Anzeigen
+ </button>
+ </div>
+ </div>
+ <p>
+ Weitere Filter:
+ </p>
<div class="row">
<div class="input-field col s12">
- <label>
- %= check_box include_manual => 1
- <span>Manuelle Einträge ohne Unterwegshalte mitberücksichtigen</span>
- </label>
+ %= 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>
+ </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>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_field 'filter_type', id => 'filter_type'
+ <label for="filter_tpye">Verkehrsmittel</label>
</div>
</div>
<div class="row">
<div class="col s12 center-align">
- <button class="btn wave-effect waves-light" type="submit" name="action" value="go">
+ <button class="btn wave-effect waves-light" type="submit">
Anzeigen
</button>
</div>
</div>
%= end
+
+<div class="row">
+ <div class="col s12">
+ <p>
+ Die eingezeichneten Routen stammen aus dem HAFAS und sind im Detail
+ oft fehlerbehaftet.
+ </p>
+ </div>
+</div>
+
+% if (@{$skipped_journeys // []}) {
+ <div class="row">
+ <div class="col s12">
+ <p>
+ Die folgenden Fahrten wurden nicht eingezeichnet:
+ </p>
+ <p>
+ <ul>
+ % for my $pair (@{$skipped_journeys}) {
+ % my ($journey, $reason) = @{$pair};
+ <li><a href="/journey/<%= $journey->{id} %>"><%= $journey->{type} %> <%= $journey->{no} %> <%= $journey->{from_name} %> → <%= $journey->{to_name} %></a>: <%= $reason %></li>
+ % }
+ </ul>
+ </p>
+ </div>
+ </div>
+% }
diff --git a/templates/journey.html.ep b/templates/journey.html.ep
index 74ad962..f5eebfc 100644
--- a/templates/journey.html.ep
+++ b/templates/journey.html.ep
@@ -4,7 +4,7 @@
<div class="card caution-color">
<div class="card-content white-text">
<span class="card-title">Fehler</span>
- <p>Zugfahrt nicht gefunden.</p>
+ <p>Fahrt nicht gefunden.</p>
</div>
</div>
</div>
@@ -14,6 +14,9 @@
<div class="row">
<div class="col s12">
<p>
+ % if (my $name = stash('username')) {
+ <b><a href="/p/<%= $name %>"><%= $name %></a></b>s
+ % }
% if ($journey->{cancelled}) {
Ausgefallene Fahrt
% }
@@ -25,17 +28,33 @@
% }
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>
+ % }
+ % else {
+ <a class="right" href="/journey/visibility?id=<%= $journey->{id} %>">
+ <i class="material-icons"><%= visibility_icon($v) %></i>
+ </a>
+ % }
+ % }
</p>
% if ($journey->{edited}) {
<p>
∗ Daten wurden manuell eingetragen
</p>
% }
- % if ($journey->{cancelled} or ($journey->{rt_arrival} and ($journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch) >= 3600)) {
+ % if (not stash('readonly') and ($journey->{cancelled} or ($journey->{rt_arrival} and ($journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch) >= 3600))) {
<div style="text-align: center; margin-bottom: 1em;">
% my $form_target = sprintf('/journey/passenger_rights/FGR %s %s %s.pdf', $journey->{sched_departure}->ymd, $journey->{type}, $journey->{no});
%= form_for $form_target => (method => 'POST') => begin
@@ -50,7 +69,7 @@
% }
<table class="striped">
<tr>
- <th scope="row">Zug</th>
+ <th scope="row">Fahrt</th>
<td>
<%= $journey->{type} %> <%= $journey->{no} %>
% if ($journey->{line}) {
@@ -104,15 +123,15 @@
</td>
</tr>
<tr>
- <th scope="row">Entfernung</th>
+ <th scope="row">Strecke</th>
<td>
% if ($journey->{skip_route}) {
<i class="material-icons right">location_off</i>
<%= numify_skipped_stations($journey->{skip_route}) %><br/>
% }
% if ($journey->{km_route} > 0.1) {
- ca. <%= sprintf('%.f', $journey->{km_route}) %> km
- (Luftlinie: <%= sprintf('%.f', $journey->{km_beeline}) %> km)
+ ca. <%= sprintf_km($journey->{km_route}) %>
+ (Luftlinie: <%= sprintf_km($journey->{km_beeline}) %>)
% }
% else {
?
@@ -123,7 +142,7 @@
</td>
</tr>
<tr>
- <th scope="row">Geschwindigkeit</th>
+ <th scope="row">Tempo</th>
<td>
% if ($journey->{skip_route}) {
<i class="material-icons right">location_off</i>
@@ -160,9 +179,36 @@
</td>
</tr>
% }
+ % if ($journey->{user_data} and $journey->{user_data}{wagongroups} and not exists $journey->{user_data}{wagons}) {
+ <tr>
+ <th scope="row">Rollmaterial</th>
+ <td class="wagons">
+ %= include '_wagons', wagongroups => $journey->{user_data}{wagongroups};
+ </td>
+ </tr>
+ % }
+ % elsif ($journey->{user_data} and $journey->{user_data}{wagons}) {
+ <tr>
+ <th scope="row">Rollmaterial</th>
+ <td class="wagons">
+ % for my $wagongroup (@{$journey->{user_data}{wagongroups} // []}) {
+ Wagenverbund <%= $wagongroup %><br/>
+ % }
+ % for my $wagon (@{$journey->{user_data}{wagons}}) {
+ <span><%= substr($wagon->{id}, 0, 2) %></span><span><%= substr($wagon->{id}, 2, 2) %></span><span><%= substr($wagon->{id}, 4, 1) %></span><span class="wagonclass"><%= substr($wagon->{id}, 5, 3) %></span><span class="wagonnum"><%= substr($wagon->{id}, 8, 3) %></span><span class="checksum"><%= substr($wagon->{id}, 11) %></span>
+ %= $wagon->{type}
+ % if ($wagon->{number}) {
+ – Wagen <%= $wagon->{number} %>
+ % }
+ <br/>
+ % }
+ </td>
+ </tr>
+ % }
<tr>
<th scope="row">Route</th>
<td>
+ % my $before = 1;
% my $within = 0;
% my $at_startstop = 0;
% for my $station (@{$journey->{route}}) {
@@ -175,8 +221,16 @@
% else {
% $at_startstop = 0;
% }
+ <span style="color: #808080;">
+ % if ($before and $station->[2]{sched_dep}) {
+ %= $station->[2]{sched_dep}->strftime('%H:%M')
+ % }
+ % elsif (not $before and $station->[2]{sched_arr}) {
+ %= $station->[2]{sched_arr}->strftime('%H:%M')
+ % }
+ </span>
% if ($at_startstop or $within) {
- <%= $station->[0] %>
+ %= $station->[0]
% }
% else {
<span style="color: #808080;"><%= $station->[0] %></span>
@@ -184,77 +238,88 @@
% if ($journey->{edited} & 0x0010) {
% }
- <br/>
- % }
- </td>
- </tr>
- % if ($journey->{user_data} and $journey->{user_data}{wagongroups} and not exists $journey->{user_data}{wagons}) {
- <tr>
- <th scope="row">Rollmaterial</th>
- <td class="wagons">
- %= include '_wagons', wagongroups => $journey->{user_data}{wagongroups};
- </td>
- </tr>
- % }
- % elsif ($journey->{user_data} and $journey->{user_data}{wagons}) {
- <tr>
- <th scope="row">Rollmaterial</th>
- <td class="wagons">
- % for my $wagongroup (@{$journey->{user_data}{wagongroups} // []}) {
- Wagenverbund <%= $wagongroup %><br/>
- % }
- % for my $wagon (@{$journey->{user_data}{wagons}}) {
- <span><%= substr($wagon->{id}, 0, 2) %></span><span><%= substr($wagon->{id}, 2, 2) %></span><span><%= substr($wagon->{id}, 4, 1) %></span><span class="wagonclass"><%= substr($wagon->{id}, 5, 3) %></span><span class="wagonnum"><%= substr($wagon->{id}, 8, 3) %></span><span class="checksum"><%= substr($wagon->{id}, 11) %></span>
- %= $wagon->{type}
- % if ($wagon->{number}) {
- – Wagen <%= $wagon->{number} %>
+ % if ($within or $at_startstop) {
+ <span style="color: #808080;">
+ % if ($before and $station->[2]{rt_dep} and $station->[2]{dep_delay}) {
+ %= sprintf('%+d', $station->[2]{dep_delay})
+ % }
+ % elsif (not $before and $station->[2]{rt_arr} and $station->[2]{arr_delay}) {
+ %= sprintf('%+d', $station->[2]{arr_delay})
+ % }
+ </span>
+ % }
+ % if ($station->[0] eq $journey->{from_name}) {
+ % $before = 0;
% }
<br/>
% }
</td>
</tr>
- % }
</table>
</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">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">edit</i>
- Bearbeiten
- </button>
- %= end
+ % if (stash('polyline_groups')) {
+ %= include '_map', station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
+ % }
+ % 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">
+ <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-text="<%= stash('share_text') %>"
+ >
+ <i class="material-icons left" aria-hidden="true">share</i> Teilen
+ </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="col s12 m6 l6 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">delete_forever</i>
- Löschen
- </a>
+ <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
+ </div>
+ <div class="col s12 m6 l6 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
+ </a>
+ </div>
</div>
- </div>
+ % }
% }
diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep
index fd71f9c..45bfb21 100644
--- a/templates/landingpage.html.ep
+++ b/templates/landingpage.html.ep
@@ -1,4 +1,5 @@
% if (is_user_authenticated()) {
+ % my $status = stash('user_status');
% if (stash('error')) {
<div class="row">
<div class="col s12">
@@ -13,21 +14,23 @@
% }
<div class="row">
<div class="col s12 statuscol">
- % my $status = get_user_status();
% if ($status->{checked_in}) {
- %= include '_checked_in', journey => $status;
+ %= include '_checked_in', journey => $status, journey_visibility => stash('journey_visibility');
% }
% elsif ($status->{cancelled}) {
+ % if ( @{stash('timeline') // [] } ) {
+ %= include '_timeline_link', timeline => stash('timeline')
+ % }
<div class="card info-color">
<div class="card-content">
- <span class="card-title">Zugausfall dokumentieren</span>
+ <span class="card-title">Ausfall dokumentieren</span>
<p>Prinzipiell wärest du nun eingecheckt in
- <%= $status->{train_type} %> <%= $status->{train_no} %>
- ab <%= $status->{dep_name} %>, doch dieser Zug fällt aus.
- </p>
- <p>Falls du den Zugausfall z.B. für ein Fahrgastrechteformular
- dokumentieren möchtest, wähle bitte jetzt die geplante
- Zielstation aus.</p>
+ %= include '_format_train', journey => $status
+ ab <%= $status->{dep_name} %>, doch diese Fahrt fällt aus.
+ </p>
+ <p>Falls du den Ausfall z.B. für Fahrgastrechte
+ dokumentieren möchtest, wähle bitte jetzt das
+ vorgesehene Ziel aus.</p>
<table>
<tbody>
% my $is_after = 0;
@@ -45,21 +48,24 @@
</div>
% }
% else {
+ % if ( @{stash('timeline') // [] } ) {
+ %= include '_timeline_link', timeline => stash('timeline')
+ % }
<div class="card">
<div class="card-content">
- <span class="card-title">Hallo, <%= current_user()->{name} %>!</span>
+ <span class="card-title">Hallo, <%= current_user->{name} %>!</span>
<p>Du bist gerade nicht eingecheckt.</p>
- <div class="geolocation">
+ <div class="geolocation" data-recent="<%= join('|', map { $_->{eva} . ';' . $_->{name} . ';' . $_->{hafas} } @{stash('recent_targets') // []} ) %>">
<button class="btn waves-effect waves-light btn-flat">Stationen in der Umgebung abfragen</button>
</div>
%= form_for 'list_departures' => begin
<div class="input-field">
- %= text_field 'station', id => 'station', class => 'autocomplete contrast-color-text', required => undef
- <label for="station">Manuelle Eingabe (Name oder DS100)</label>
+ %= text_field 'station', id => 'station', class => 'autocomplete contrast-color-text', autocomplete => 'off', required => undef
+ <label for="station">Manuelle Eingabe</label>
</div>
<div class="center-align">
<button class="btn waves-effect waves-light btn-flat" type="submit" name="action" value="departures">
- <i class="material-icons left">send</i>
+ <i class="material-icons left" aria-hidden="true">send</i>
Abfahrten
</button>
</div>
@@ -69,8 +75,8 @@
% }
</div>
</div>
- <h1>Letzte Fahrten</h1>
- %= include '_history_trains', date_format => '%d.%m', journeys => [get_user_travels(limit => 5, with_datetime => 1)];
+ <h2 style="margin-left: 0.75rem;">Letzte Fahrten</h2>
+ %= include '_history_trains', date_format => '%d.%m.%Y', journeys => [journeys->get(uid => current_user->{id}, limit => 5, with_datetime => 1)];
% }
% else {
<div class="row">
@@ -79,8 +85,8 @@
Travelynx erlaubt das Einchecken in Züge im Netz der Deutschen
Bahn. So können die eigenen Fahrten später inklusive Echtzeitdaten
und eingetragenen Servicemeldungen nachvollzogen und brennende
- Fragen wie „Wie viele Stunden habe ich letzten Monat im Zug
- vebracht?“ beantwortet werden.
+ Fragen wie „Wie viele Stunden war ich letzten Monat unterwegs?“
+ beantwortet werden.
</p>
<p>
Die Idee dazu kommt von <a
@@ -91,10 +97,12 @@
<ul>
<li>Protokoll von Fahrplan- und Echtzeitdaten an Start- und
Zielbahnhof</li>
- <li>Web-Hooks und <a href="/api">API</a> zum automatisierten Auslesen des aktuellen Status</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: Öffentlich sichtbarer Reisestatus</li>
+ <li>Optional: Öffentlicher Reisestatus und öffentliche Angaben zu vergangenen Fahrten</li>
+ <li>Optional: Verknüpfung mit Träwelling</li>
</ul>
</p>
<p>
@@ -111,12 +119,12 @@
<div class="col s1 m1 l3">
</div>
<div class="col s10 m10 l6 center-align">
- <a href="/register" class="waves-effect waves-light btn"><i class="material-icons left">add</i>Registrieren</a>
- <a href="/login" class="waves-effect waves-light btn"><i class="material-icons left">account_circle</i>Anmelden</a>
+ % 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="/login" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">account_circle</i>Anmelden</a>
</div>
<div class="col s1 m1 l3">
</div>
</div>
% }
-
-%= include '_footer', version => stash('version')
diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep
index bd9bf8d..fbb26ef 100644
--- a/templates/layouts/default.html.ep
+++ b/templates/layouts/default.html.ep
@@ -10,14 +10,17 @@
% while (my ($key, $value) = each %{stash('twitter') // {}}) {
<meta name="twitter:<%= $key %>" content="<%= $value %>">
% }
- % my $av = 'v32'; # asset version
+ % while (my ($key, $value) = each %{stash('opengraph') // {}}) {
+ <meta property="og:<%= $key %>" content="<%= $value %>">
+ % }
+ % my $av = 'v72'; # 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">
- <link rel="apple-touch-icon" href="/static/<%= $av %>/icons/icon-120x120.png">
- <link rel="apple-touch-icon" sizes="180x180" href="/static/<%= $av %>/icons/icon-180x180.png">
- <link rel="apple-touch-icon" sizes="152x152" href="/static/<%= $av %>/icons/icon-152x152.png">
- <link rel="apple-touch-icon" sizes="167x167" href="/static/<%= $av %>/icons/icon-167x167.png">
+ <link rel="apple-touch-icon" href="/static/<%= $av %>/icons/touch-icon-120x120.png">
+ <link rel="apple-touch-icon" sizes="180x180" href="/static/<%= $av %>/icons/touch-icon-180x180.png">
+ <link rel="apple-touch-icon" sizes="152x152" href="/static/<%= $av %>/icons/touch-icon-152x152.png">
+ <link rel="apple-touch-icon" sizes="167x167" href="/static/<%= $av %>/icons/touch-icon-167x167.png">
<link rel="manifest" href="/static/<%= $av %>/manifest.json">
% if (session('theme') and session('theme') eq 'dark') {
%= stylesheet "/static/${av}/css/dark.min.css", id => 'theme'
@@ -43,37 +46,35 @@
currentTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
addStyleSheet(currentTheme, 'theme');
-
- function toggleTheme() {
- currentTheme = otherTheme[currentTheme] || 'light';
- localStorage.setItem('theme', currentTheme);
- addStyleSheet(currentTheme, 'theme');
- }
</script>
%= stylesheet "/static/${av}/css/material-icons.css"
- %= stylesheet "/static/${av}/css/local.css"
% if (stash('with_map')) {
%= stylesheet "/static/${av}/leaflet/leaflet.css"
% }
%= javascript "/static/${av}/js/jquery-3.4.1.min.js"
%= javascript "/static/${av}/js/materialize.min.js"
- %= javascript "/static/${av}/js/travelynx-actions.min.js"
+ % my $min = ".min";
+ % if (app->mode eq 'development') {
+ % $min = q{};
+ % }
+ %= javascript "/static/${av}/js/travelynx-actions${min}.js"
% if (stash('with_geolocation')) {
- %= javascript "/static/${av}/js/geolocation.min.js"
+ %= javascript "/static/${av}/js/geolocation${min}.js"
% }
% if (stash('with_autocomplete')) {
- %= javascript "/static/${av}/js/autocomplete.min.js"
+ %= javascript "/dyn/${av}/autocomplete.js", defer => undef
% }
% if (stash('with_map')) {
%= javascript "/static/${av}/leaflet/leaflet.js"
% }
</head>
+% my $acc = is_user_authenticated() && current_user();
<body>
<div class="navbar-fixed">
<nav class="deep-purple">
<div class="nav-wrapper container">
- <a href="/" class="brand-logo left">travelynx</a>
+ <a href="/" class="brand-logo left"><span class="ca">tr</span><span class="cb">av</span><span class="cc">e</span><span class="cd">ly</span><span class="ce">nx</span></a>
<ul id="nav-mobile" class="right">
<li class="loading">
<div class="preloader-wrapper small" style="margin-top: 0.5em; margin-bottom: -1em;">
@@ -88,15 +89,12 @@
</div>
</div>
</li>
- <li class="waves-effect waves-light">
- <a onClick="javascript:toggleTheme()"><i class="material-icons">invert_colors</i></a>
- </li>
- % if (is_user_authenticated()) {
- <li class="<%= navbar_class('/history') %>"><a href='/history' title="History"><i class="material-icons">history</i></a></li>
- <li class="<%= navbar_class('/account') %>"><a href="/account" title="Account"><i class="material-icons">account_circle</i></a></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('/account') %>"><a href="/account" title="Account"><i class="material-icons" aria-label="Account"><%= $acc->{notifications} ? 'notifications' : 'account_circle' %></i></a></li>
% }
% else {
- <li class="<%= navbar_class('/about') %>"><a href='/about' title="About"><i class="material-icons">info_outline</i></a></li>
+ <li class="<%= navbar_class('/about') %>"><a href='/about' title="Über Travelynx"><i class="material-icons" aria-label="Über Travelynx">info_outline</i></a></li>
% }
</ul>
</div>
@@ -114,16 +112,43 @@
</div>
% }
+% if (app->config->{announcement}) {
<div class="container">
- % if (is_user_authenticated()) {
- % my $acc = current_user();
- % if ($acc and $acc->{deletion_requested}) {
- %= include '_deletion_note', timestamp => $acc->{deletion_requested}
- % }
+ <div class="row">
+ <div class="col s12 caution-color white-text">
+ %= app->config->{announcement}
+ </div>
+ </div>
+</div>
+% }
+
+<div class="container">
+ % if ($acc and $acc->{deletion_requested}) {
+ %= include '_deletion_note', timestamp => $acc->{deletion_requested}
% }
%= content
+ <div class="row" style="margin-top: 5em;">
+ <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>
+ <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span>
+ <a href="/impressum">Datenschutz</a>
+ <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span>
+ <a href="/legend">Legende</a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12 center-align grey-text config">
+ Farbschema:
+ <a onClick="javascript:setTheme('light')">hell</a>
+ ·
+ <a onClick="javascript:setTheme('dark')">dunkel</a>
+ ·
+ <a onClick="javascript:setTheme('default')">automatisch</a>
+ </div>
+ </div>
</div>
-
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
diff --git a/templates/legend.html.ep b/templates/legend.html.ep
new file mode 100644
index 0000000..73fded9
--- /dev/null
+++ b/templates/legend.html.ep
@@ -0,0 +1,111 @@
+<div class="row">
+ <div class="col s12">
+ <h2>Legende</h2>
+ <p>travelynx verwendet bei Angaben zu Zügen und Stationen die folgenden Symbole.</p>
+ <h3>Abfahrtstafel und Route</h3>
+ <table class="striped">
+ <tbody>
+ <tr>
+ <td><i class="material-icons">gps_off</i></td>
+ <td>Keine Echtzeitdaten vorhanden. Bei den angegebenen Zeiten handelt es sich um Angaben aus dem Fahrplan.</td>
+ </tr>
+ <tr>
+ <td>(HH:MM)</td>
+ <td>Ein Einstieg (Abfahrtstafel) bzw. Ausstieg (Route) ist an dieser Station möglicherweise nicht vorgesehen.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">train</i></td>
+ <td>Backend: DB IRIS. Bevorzugte Datenquelle für (mindestens teilweise) innerdeutsche Zugfahrten.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">directions</i></td>
+ <td>Backend: DB HAFAS. Bevorzugte Datenquelle für Nahverkehr und vollständig außerdeutsche Zugfahrten. Weniger detailliert als IRIS.</td>
+ </tr>
+ </tbody>
+ </table>
+ <h3>Anschlusszüge</h3>
+ <table class="striped">
+ <tbody>
+ <tr>
+ <td><i class="material-icons">directions_run</i></td>
+ <td>Knapper Umstieg. Anschluss wird möglicherweise nicht erreicht.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">warning</i></td>
+ <td>Der Zug ist überbesetzt. Möglicherweise sind keine freien Sitzplätze vorhanden.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">info_outline</i></td>
+ <td>Eingeschränkte Barrierefreihet, z.B. fehlendes oder defektes rollstuhlgerechtes WC.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">remove</i></td>
+ <td>Mindestens ein Wagen fehlt.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">compare_arrows</i></td>
+ <td>Abweichende Wagenreihung.</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">portable_wifi_off</i></td>
+ <td>WLAN ganz oder teilweise ausgefallen.</td>
+ </tr>
+ </tbody>
+ </table>
+ <h3>Auslastung</h3>
+ <p>Die erwartete Auslastung der ersten und zweiten Wagenklasse wird bei Anschlussvorschlägen sowie bei Unterwegshalten des aktuellen Zuges angegeben, sofern verfügbar.</p>
+ <table class="striped">
+ <tbody>
+ <tr>
+ <td><i class="material-icons">help_outline</i></td>
+ <td>Auslastung unbekannt</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">person_outline</i></td>
+ <td>Niedrige Auslastung</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">people</i></td>
+ <td>Hohe Auslastung</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">priority_high</i></td>
+ <td>Sehr hohe Auslastung</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons">not_interested</i></td>
+ <td>Der Zug ist ausgebucht</td>
+ <tr>
+ </tbody>
+ </table>
+ <h3>Profil und Timeline</h3>
+ <table class="striped">
+ <tbody>
+ <tr>
+ <td><i class="material-icons"><%= visibility_icon('public') %></i></td>
+ <td>Öffentlicher Checkin</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons"><%= visibility_icon('travelynx') %></i></td>
+ <td>Nur für Angemeldete</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons"><%= visibility_icon('followers') %></i></td>
+ <td>Nur für Follower</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons"><%= visibility_icon('unlisted') %></i></td>
+ <td>Nur mit Link / Token</td>
+ </tr>
+ <tr>
+ <td><i class="material-icons"><%= visibility_icon('private') %></i></td>
+ <td>Privater Checkin</td>
+ </tr>
+ <!-- <tr>
+ <td><i class="material-icons">person_add</i></td>
+ <td>Mitfahren (Checkin übernehmen)</td>
+ <tr> -->
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/templates/login.html.ep b/templates/login.html.ep
index a5aa8e3..ce89813 100644
--- a/templates/login.html.ep
+++ b/templates/login.html.ep
@@ -44,6 +44,15 @@
</div>
</div>
% }
+ % elsif ($from eq 'auth_required') {
+ <div class="card">
+ <div class="card-content">
+ <span class="card-title">Login notwendig</span>
+ <p>Die aufgerufene Seite ist nur mit travelynx-Account zugänglich.</p>
+ <p><a href="/">Über travelynx</a> · <a href="/register">Registrieren</a></p>
+ </div>
+ </div>
+ % }
</div>
</div>
% }
@@ -55,7 +64,7 @@
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">account_circle</i>
- %= text_field 'user', id => 'user', class => 'validate', required => undef, maxlength => 60, autocomplete => 'username'
+ %= text_field 'user', id => 'user', class => 'validate', required => undef, pattern => '[0-9a-zA-Z_-]+', maxlength => 60, autocomplete => 'username'
<label for="user">Account</label>
</div>
<div class="input-field col s12">
@@ -87,6 +96,11 @@
<div class="col s3 m3 l3">
</div>
</div>
+ % 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>
+ </div>
+ </div>
+ % }
%= end
-
-%= include '_footer', version => stash('version')
diff --git a/templates/not_found.html.ep b/templates/not_found.html.ep
index c2bc09f..411080a 100644
--- a/templates/not_found.html.ep
+++ b/templates/not_found.html.ep
@@ -3,7 +3,12 @@
<div class="card caution-color">
<div class="card-content white-text">
<span class="card-title">404 Not Found</span>
- <p>Diese Seite gibt es hier nicht.</p>
+ % if (my $m = stash('message')) {
+ <p><%= $m %></p>
+ % }
+ % else {
+ <p>Diese Seite gibt es hier nicht.</p>
+ % }
</div>
</div>
</div>
diff --git a/templates/privacy.html.ep b/templates/privacy.html.ep
index e8e6459..8cd3a78 100644
--- a/templates/privacy.html.ep
+++ b/templates/privacy.html.ep
@@ -1,63 +1,176 @@
<h1>Öffentliche Daten</h1>
<div class="row">
<div class="col s12">
- Hier kannst du auswählen, welche Aspekte deines Accounts bzw. deiner
- Bahnfahrten öffentlich einsehbar sind. Öffentliche Daten sind
- grundsätzlich für <i>alle</i> einsehbar, die die (leicht erratbare) URL
- kennen.
+ Hier kannst du auswählen, welche Personengruppen deine Fahrten
+ bei travelynx einsehen können. Zusätzlich kannst du die
+ Sichtbarkeit vergangener Fahrten auf die letzten vier Wochen
+ einschränken. Nach dem Einchecken hast du im Checkin-Fenster
+ die Möglichkeit, für die aktuelle Fahrt eine abweichende Sichtbarkeit
+ einzustellen.
</div>
</div>
%= form_for '/account/privacy' => (method => 'POST') => begin
-<h2>Aktueller Status</h2>
- %= csrf_field
+%= csrf_field
+<h2>Fahrten</h2>
<div class="row">
<div class="input-field col s12">
- <label>
- %= radio_button status_level => 'private'
- <span>Nicht sichtbar</span>
- </label>
+ <div>
+ <label>
+ %= radio_button status_level => 'public'
+ <span><i class="material-icons left"><%= visibility_icon('public') %></i>Öffentlich: Im Profil und auf der Statusseite eingebunden und beliebig zugänglich.</span>
+ </label>
+ </div>
</div>
</div>
<div class="row">
<div class="input-field col s12">
- <label>
- %= radio_button status_level => 'intern'
- <span>Nur für angemeldete Accounts</span>
- </label>
+ <div>
+ <label>
+ %= radio_button status_level => 'travelynx'
+ <span><i class="material-icons left"><%= visibility_icon('travelynx') %></i>Intern: Personen, die dir folgen, die auf dieser Seite angemeldet sind oder denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'followers'
+ <span><i class="material-icons left"><%= visibility_icon('followers') %></i>Follower: Personen, die dir folgen oder denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'unlisted'
+ <span><i class="material-icons left"><%= visibility_icon('unlisted') %></i>Verlinkbar: Personen, denen du mithilfe der Teilen-Funktion einen Link schickst.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button status_level => 'private'
+ <span><i class="material-icons left"><%= visibility_icon('private') %></i>Privat: nur für dich sichtbar.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= check_box past_status => 1
+ <span>Wenn nicht eingecheckt: letztes Fahrtziel anzeigen, sofern die zugehörige Reise für die aufrufende Person sichtbar ist. Caveat: Die derzeitige Implementierung dieses Features gibt preis, ob deine letzte Fahrt öffentlich / lokal sichtbar war (→ Ziel angegeben) oder nicht (→ kein Ziel angegeben).</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12">
+ Wenn du (mit passender Sichtbarkeit) eingecheckt bist, werden unter
+ <a href="/status/<%= $name %>">/status/<%= $name %></a> sowie <a
+ href="/p/<%= $name %>">/p/<%= $name %></a> Fahrt, Start- und
+ Zielstation sowie Abfahrts- und Ankunftszeit gezeigt. Andernfalls
+ wird angegeben, dass du gerade nicht eingecheckt seist.
+ </div>
+ </div>
+<h2>Vergangene Fahrten</h2>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button history_level => 'public'
+ <span><i class="material-icons left"><%= visibility_icon('public') %></i>Öffentlich: Beliebig zugänglich.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button history_level => 'travelynx'
+ <span><i class="material-icons left"><%= visibility_icon('travelynx') %></i>Intern: Personen, die dir folgen oder die auf dieser Seite angemeldet sind.</span>
+ </label>
+ </div>
</div>
</div>
<div class="row">
<div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button history_level => 'followers'
+ <span><i class="material-icons left"><%= visibility_icon('followers') %></i>Follower: Personen, die dir folgen.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button history_level => 'private'
+ <span><i class="material-icons left"><%= visibility_icon('private') %></i>Privat: nur für dich sichtbar.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s12">
+ Diese Einstellung bestimmt die Sichtbarkeit vergangener Fahrten
+ unter <a href="/p/<%= $name %>">/p/<%= $name %></a>. Vergangene
+ Fahrten, die über die Standardeinstellung (siehe oben) oder per
+ individueller Einstellung für die aufrufende Person sichtbar sind,
+ werden hier verlinkt. Derzeit werden nur die letzten zehn Fahrten
+ angezeigt; in Zukunft wird dies ggf. auf sämtliche Fahrten im
+ gewählten Zeitraum erweitert.
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button history_age => 'month'
+ <span>Letzte vier Wochen</span>
+ </label>
+ </div><div>
<label>
- %= radio_button status_level => 'extern'
- <span>Öffentlich</span>
+ %= radio_button history_age => 'infinite'
+ <span>Alle Fahrten</span>
</label>
+ </div>
</div>
</div>
<div class="row">
<div class="col s12">
- Hier kannst du auswählen, ob dein aktueller Status unter <a
- href="/status/<%= $name %>">/status/<%= $name %></a> abrufbar ist.
- Wenn du eingecheckt bist, werden dort Zug, Start- und Zielstation,
- Abfahrts- und Ankunftszeit gezeigt; andernfalls lediglich der
- Zielbahnhof der letzten Reise. Wann die letzte Reise beendet wurde,
- wird bewusst nicht angegeben.
+ Hier kannst du auswählen, ob alle deiner vergangenen Fahrten für
+ Profil und Detailseiten in Frage kommen oder nur die letzten vier
+ Wochen zugänglich sein sollen. Sofern du sie auf die letzten vier
+ Wochen beschränkst, sind ältere Fahrten nur über einen mit
+ Hilfe des „Teilen“-Knopfs erstellten Links zugänglich.
</div>
</div>
+<h2>Sonstiges</h2>
<div class="row">
<div class="input-field col s12">
<label>
%= check_box public_comment => 1
- <span>Mit Kommentar</span>
+ <span>Kommentare anzeigen</span>
</label>
</div>
</div>
<div class="row">
<div class="col s12">
- Wenn aktiv, wird in deinem aktuellen Status auch der optionale
- Freitext-Kommentar der Zugfahrt gezeigt. Wenn du gerade nicht
- eingecheckt bist oder dein aktueller Status nicht öffentlich ist,
- hat dieses Feld keine Auswirkungen.
+ Wenn aktiv, sind von dir eingetragene Freitext-Kommentare in deinem
+ aktuellen Status sowie bei deinen vergangenen Fahrten sichtbar.
+ Diese Einstellung kann nicht pro Fahrt verändert werden.
</div>
</div>
<div class="row">
diff --git a/templates/profile.html.ep b/templates/profile.html.ep
new file mode 100644
index 0000000..6f78ea0
--- /dev/null
+++ b/templates/profile.html.ep
@@ -0,0 +1,92 @@
+% if (stash('error')) {
+ <div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">Fehler</span>
+ <p><%= stash('error') %></p>
+ </div>
+ </div>
+ </div>
+ </div>
+% }
+<div class="row">
+ <div class="col s12">
+ <div class="card">
+ <div class="card-content">
+ <span class="card-title"><%= $name %>
+ % if ($following and $follows_me) {
+ <i class="material-icons right">group</i>
+ % }
+ % elsif ($follow_reqs_me) {
+ <span class="right">
+ <a href="/account/social/follow-requests-received"><i class="material-icons right">notifications</i></a>
+ </span>
+ % }
+ % elsif ($is_self) {
+ <a href="/account/profile"><i class="material-icons right">edit</i></a>
+ % }
+ </span>
+ % if ($bio) {
+ %== $bio
+ % }
+ % if (@{$metadata // []}) {
+ <table class="striped">
+ % for my $entry (@{$metadata}) {
+ <tr>
+ <th scope="row"><%= $entry->{key} %></th>
+ <td scope="row"><%== $entry->{value}{html} %></td>
+ </tr>
+ % }
+ </table>
+ % }
+ </div>
+ % if ($following or $follow_requested or $can_follow or $can_request_follow) {
+ <div class="card-action <%= ($can_follow or $can_request_follow) ? 'right-align' : q{} %>">
+ %= form_for "/social-action" => (method => 'POST') => begin
+ %= csrf_field
+ %= hidden_field target => $uid
+ %= hidden_field redirect_to => 'profile'
+ % if ($following) {
+ <button class="btn-flat waves-effect waves-light" type="submit" name="action" value="unfollow">
+ Nicht mehr folgen
+ </button>
+ % }
+ % elsif ($follow_requested) {
+ <button class="btn-flat waves-effect waves-light" type="submit" name="action" value="cancel_follow_request">
+ Folge-Anfrage zurücknehmen
+ </button>
+ % }
+ % elsif ($can_follow or $can_request_follow) {
+ <button class="btn-flat waves-effect waves-light" type="submit" name="action" value="follow_or_request">
+ <i class="material-icons left" aria-hidden="true">person_add</i>
+ % if ($follows_me) {
+ Zurückfolgen
+ % }
+ % else {
+ Folgen
+ % }
+ % if ($can_request_follow) {
+ anfragen
+ % }
+ </button>
+ % }
+ %= end
+ </div>
+ % }
+ </div>
+ </div>
+</div>
+<div class="row">
+ <div class="col s12 publicstatuscol" data-user="<%= $name %>" data-profile="1">
+ %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey, from_profile => 1
+ </div>
+</div>
+% if ($journeys and @{$journeys}) {
+ <div class="row">
+ <div class="col s12">
+ <h2>Vergangene Fahrten</h2>
+ </div>
+ </div>
+ %= include '_history_trains', date_format => '%d.%m.%Y', link_prefix => "/p/${name}/j/", journeys => $journeys;
+% }
diff --git a/templates/register.html.ep b/templates/register.html.ep
index 1983e92..ee344f9 100644
--- a/templates/register.html.ep
+++ b/templates/register.html.ep
@@ -3,6 +3,7 @@
% }
%= form_for '/register' => (method => 'POST') => begin
%= csrf_field
+ %= hidden_field dt => DateTime->now(time_zone => 'Europe/Berlin')->epoch
<div class="row">
<div class="input-field col l6 m12 s12">
<i class="material-icons prefix">account_circle</i>
@@ -16,12 +17,12 @@
</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, autocomplete => 'new-password'
+ %= password_field 'password', id => 'password', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password'
<label for="password">Passwort</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, autocomplete => 'new-password'
+ %= password_field 'password2', id => 'password2', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password'
<label for="password2">Passwort wiederholen</label>
</div>
</div>
@@ -46,12 +47,14 @@
Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich.
</p>
<p>
- Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung
- und für die "Passwort vergessen"-Funktionalität verwendet und nicht
- an Dritte weitergegeben. Die <a
- href="/impressum">Datenschutzerklärung</a> beschreibt weitere
- erhobene Daten sowie deren Zweck und Speicherfristen.
- Accounts werden nach einem Jahr ohne Aktivität automatisch gelöscht.
+ 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. Die <a href="/impressum">Datenschutzerklärung</a>
+ beschreibt weitere erhobene Daten sowie deren Zweck und
+ Speicherfristen. 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.
</p>
<p>
Bitte beachten: Travelynx ist ein privat betriebenes Projekt ohne
@@ -61,5 +64,3 @@
</p>
</div>
</div>
-
-%= include '_footer', version => stash('version')
diff --git a/templates/social.html.ep b/templates/social.html.ep
new file mode 100644
index 0000000..dafabc4
--- /dev/null
+++ b/templates/social.html.ep
@@ -0,0 +1,68 @@
+<h1>Interaktion</h1>
+<div class="row">
+ <div class="col s12">
+ Hier kannst du einstellen, ob und wie dir Accounts folgen können.
+ Die hier vorgenommenen Einstellungen haben keinen Einfluss
+ darauf, ob/wie du anderen Accounts folgen kannst.
+ </div>
+</div>
+%= form_for '/account/social' => (method => 'POST') => begin
+%= csrf_field
+<h2>Folgen</h2>
+ <div class="row">
+ <div class="input-field col s12">
+ <p>
+ Accounts die dir folgen können alle Checkins sehen, die nicht als privat oder nur mit Link zugänglich vermerkt sind.
+ Später werden sie zusätzlich die Möglichkeit haben, deinen aktuellen Checkin (sofern sichtbar) als Teil einer Übersicht über die Checkins aller gefolgten Accounts direkt anzusehen (analog zur Timeline im Fediverse).
+ </p>
+ <p>
+ Du hast jederzeit die Möglichkeit, Accounts aus deiner Followerliste zu entfernen, Folge-Anfragen abzulehnen oder Accounts zu blockieren, so dass sie dir weder folgen noch neue Folge-Anfragen stellen können.
+ </p>
+ <p>
+ Angaben zu folgenden und gefolgten Accounts sind grundsätzlich nur für dich sichtbar.
+ </p>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button accept_follow => 'yes'
+ <span>Andere Accounts können dir direkt (ohne eine Anfrage zu stellen) folgen.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button accept_follow => 'request'
+ <span>Andere Accounts können dir Folge-Anfragen stellen. Du musst sie explizit annehmen, bevor sie dir folgen.</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button accept_follow => 'no'
+ <span>Accounts können dir nicht folgen. Accounts, die dir bereits folgen, werden hiervon nicht berührt.</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/social_list.html.ep b/templates/social_list.html.ep
new file mode 100644
index 0000000..686e5c3
--- /dev/null
+++ b/templates/social_list.html.ep
@@ -0,0 +1,259 @@
+%= form_for "/social-action" => (method => 'POST') => begin
+%= csrf_field
+%= hidden_field redirect_to => '/account'
+% my $count = scalar @{$entries};
+% if ($type eq 'follow-requests-received') {
+ <div class="row">
+ <div class="col s12">
+ <h2>Erhaltene Folge-Anfragen</h2>
+ </div>
+ </div>
+ % if ($notifications) {
+ <div class="row center-align">
+ <div class="col s12">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="clear_notifications">
+ <i class="material-icons left" aria-hidden="true">notifications_off</i> Als gelesen markieren
+ </button>
+ </div>
+ </div>
+ % }
+ <div class="row center-align">
+ <div class="col s4">
+ <i class="material-icons">block</i><br/> Blockieren
+ </div>
+ <div class="col s4">
+ <i class="material-icons">cancel</i><br/> Ablehnen
+ </div>
+ <div class="col s4">
+ <i class="material-icons">check</i><br/> Annehmen
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s12">
+ <button class="btn red waves-effect waves-light" type="submit" name="reject_follow_request" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">cancel</i> Alle ablehnen
+ </button>
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s12">
+ <button class="btn waves-effect waves-light" type="submit" name="accept_follow_request" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">check</i> Alle annehmen
+ </button>
+ </div>
+ </div>
+ <!--
+ <div class="row center-align">
+ <div class="col s6">
+ <button class="btn red waves-effect waves-light" type="submit" name="block" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">block</i> Alle blockieren
+ </button>
+ </div>
+ </div>
+ -->
+% }
+% elsif ($type eq 'follow-requests-sent') {
+ <div class="row">
+ <div class="col s12">
+ <h2>Gestellte Folge-Anfragen</h2>
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s12">
+ <i class="material-icons">cancel</i><br/> Zurücknehmen
+ </div>
+ </div>
+% }
+% elsif ($type eq 'followers') {
+ <div class="row">
+ <div class="col s12">
+ % if ($count == 1) {
+ <h2>Dir folgt ein Account</h2>
+ % }
+ % else {
+ <h2>Dir folgen <%= $count %> Accounts</h2>
+ % }
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s4">
+ <i class="material-icons">block</i><br/> Blockieren
+ </div>
+ <div class="col s4">
+ <i class="material-icons">remove</i><br/> Entfernen
+ </div>
+ <div class="col s4">
+ <i class="material-icons">person_add</i><br/> Zurückfolgen
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s4">
+ </div>
+ <div class="col s4">
+ <i class="material-icons">access_time</i><br/> Folgen angefragt
+ </div>
+ <div class="col s4">
+ <i class="material-icons">group</i><br/> Du folgst diesem Account
+ </div>
+ </div>
+ <!--
+ <div class="row center-align">
+ <div class="col s6">
+ <button class="btn grey waves-effect waves-light" type="submit" name="remove_follower" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">remove</i> Alle entfernen
+ </button>
+ </div>
+ <div class="col s6">
+ <button class="btn waves-effect waves-light" type="submit" name="follow_or_request" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">group_add</i> Allen zurückfolgen
+ </button>
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s6">
+ <button class="btn red waves-effect waves-light" type="submit" name="block" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">block</i> Alle blockieren
+ </button>
+ </div>
+ </div>
+ -->
+% }
+% elsif ($type eq 'follows') {
+ <div class="row">
+ <div class="col s12">
+ % if ($count == 1) {
+ <h2>Du folgst einem Account</h2>
+ % }
+ % else {
+ <h2>Du folgst <%= $count %> Accounts</h2>
+ % }
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s6">
+ <i class="material-icons">group</i><br/> Folgt dir
+ </div>
+ <div class="col s6">
+ <i class="material-icons">remove</i><br/> Nicht mehr folgen
+ </div>
+ </div>
+ <!--
+ <div class="row center-align">
+ <div class="col s12">
+ <button class="btn grey waves-effect waves-light" type="submit" name="unfollow" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">remove</i> Alle entfernen
+ </button>
+ </div>
+ </div>
+ -->
+% }
+% elsif ($type eq 'blocks') {
+ <div class="row">
+ <div class="col s12">
+ <h2>Blockierte Accounts</h2>
+ <p>
+ Blockierte Accounts können dir nicht folgen und keine Folge-Anfragen stellen.
+ Sie haben weiterhin Zugriff auf deine als öffentlich oder travelynx-intern markierten Checkins.
+ </p>
+ </div>
+ </div>
+ <div class="row center-align">
+ <div class="col s12">
+ <i class="material-icons">remove</i><br/>Entblockieren
+ </div>
+ </div>
+ <!--
+ <div class="row center-align">
+ <div class="col s12">
+ <button class="btn grey waves-effect waves-light" type="submit" name="unblock" value="<%= join(q{,}, map { $_->{id} } @{$entries}) %>">
+ <i class="material-icons left" aria-hidden="true">remove</i> Alle entblockieren
+ </button>
+ </div>
+ </div>
+ -->
+% }
+%= end
+
+<div class="row">
+ <div class="col s12">
+ %= form_for "/social-action" => (method => 'POST') => begin
+ %= csrf_field
+ %= hidden_field redirect_to => "/account/social/$type"
+ <table class="striped">
+ % for my $entry (@{$entries}) {
+ <tr>
+ <td><a href="/p/<%= $entry->{name} %>"><%= $entry->{name} %></a></td>
+ % if ($type eq 'follow-requests-received') {
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="block" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="blockieren">block</i>
+ </button>
+ </td>
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="reject_follow_request" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="ablehnen">cancel</i>
+ </button>
+ </td>
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="accept_follow_request" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="annehmen">check</i>
+ </button>
+ </td>
+ % }
+ % elsif ($type eq 'follow-requests-sent') {
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="cancel_follow_request" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="zurücknehmen">cancel</i>
+ </button>
+ </td>
+ % }
+ % elsif ($type eq 'followers') {
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="block" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="blockieren">block</i>
+ </button>
+ </td>
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="remove_follower" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="entfernen">remove</i>
+ </button>
+ </td>
+ <td class="right-align">
+ % if ($entry->{following_back}) {
+ <i class="material-icons" aria-label="ihr folgt euch gegenseitig">group</i>
+ % }
+ % elsif ($entry->{followback_requested}) {
+ <i class="material-icons" aria-label="Zurückfolgen angefragt">access_time</i>
+ % }
+ % elsif ($entry->{can_follow_back} or $entry->{can_request_follow_back}) {
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="follow_or_request" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="zurückfolgen">person_add</i>
+ </button>
+ % }
+ </td>
+ % }
+ % elsif ($type eq 'follows') {
+ <td class="right-align">
+ % if ($entry->{following_back}) {
+ <i class="material-icons" aria-label="ihr folgt euch gegenseitig">group</i>
+ % }
+ </td>
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="unfollow" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="entfolgen">remove</i>
+ </button>
+ </td>
+ % }
+ % elsif ($type eq 'blocks') {
+ <td class="right-align">
+ <button class="btn-flat blue-text waves-effect waves-light" type="submit" name="unblock" value="<%= $entry->{id} %>">
+ <i class="material-icons" aria-label="von Blockliste entefrnen">remove</i>
+ </button>
+ </td>
+ % }
+ </tr>
+ % }
+ </table>
+ %= end
+ </div>
+</div>
diff --git a/templates/timeline-checked-in.html.ep b/templates/timeline-checked-in.html.ep
new file mode 100644
index 0000000..0ed492e
--- /dev/null
+++ b/templates/timeline-checked-in.html.ep
@@ -0,0 +1,3 @@
+<div class="timeline-in-transit">
+ %= include '_timeline-checked-in', journeys => $journeys
+</div>
diff --git a/templates/traewelling.html.ep b/templates/traewelling.html.ep
new file mode 100644
index 0000000..c1f2b7d
--- /dev/null
+++ b/templates/traewelling.html.ep
@@ -0,0 +1,256 @@
+% if (my $invalid = stash('invalid')) {
+ %= include '_invalid_input', invalid => $invalid
+% }
+
+<h1>Träwelling</h1>
+
+% if (flash('new_traewelling')) {
+ <div class="row">
+ <div class="col s12">
+ % if ($traewelling->{token}) {
+ <div class="card success-color">
+ <div class="card-content white-text">
+ <span class="card-title">Träwelling verknüpft</span>
+ % my $user = $traewelling->{data}{user_name} // '???';
+ <p>Dein travelynx-Account ist nun mit dem Träwelling-Account <b>@<%= $user %></b> verbunden.</p>
+ </div>
+ </div>
+ % }
+ % elsif (my $login_err = flash('login_error')) {
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">Login-Fehler</span>
+ <p>Der Login bei Träwelling ist fehlgeschlagen: <%= $login_err %></p>
+ </div>
+ </div>
+ % }
+ % elsif (my $logout_err = stash('logout_error')) {
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">Logout-Fehler</span>
+ <p>Der Logout bei Träwelling ist fehlgeschlagen: <%= $logout_err %>.
+ Dein Token bei travelynx wurde dennoch gelöscht, so
+ dass nun kein Zugriff von travelynx auf Träwelling mehr
+ möglich ist. In den <a
+ href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
+ kannst du ihn vollständig löschen.</p>
+ </div>
+ </div>
+ % }
+ </div>
+ </div>
+% }
+
+<div class="row">
+ <div class="col s12">
+ <div class="card purple">
+ <div class="card-content white-text">
+ <span class="card-title">Eingeschränkte Synchronisierung</span>
+ <p>
+ Träwelling und travelynx setzen unterschiedliche Schwerpunkte und haben unterschiedliche Features.
+ Kombiniert mit der Vielzahl an möglichen Randfällen heißt das, dass die Synchronisierung nicht immer funktioniert.
+ Diese Einschränkung ist bekannt und wird voraussichtlich bestehen bleiben.
+ </p>
+ <p>
+ Bei hohen Verspätungen, Ausfällen und nachträglichen Checkin-Änderungen ist die Synchronisierung u.U. nicht möglich und muss von Hand vorgenommen werden.
+ travelynx-Hooks werden bei via Träwelling vorgenommenen Checkins nicht ausgelöst.
+ </p>
+ </div>
+ <div class="card-action">
+ <a href="https://github.com/derf/travelynx/issues" class="waves-effect waves-light btn-flat white-text">
+ <i class="material-icons left" aria-hidden="true">bug_report</i>Bug melden
+ </a>
+ </div>
+ </div>
+ </div>
+</div>
+
+% if ($traewelling->{token} and ($traewelling->{expired} or $traewelling->{expiring})) {
+ <div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ % if ($traewelling->{expired}) {
+ <span class="card-title">Token abgelaufen</span>
+ % }
+ % else {
+ <span class="card-title">Token läuft bald ab</span>
+ % }
+ <p>Melde deinen travelynx-Account von Träwelling ab und
+ verbinde ihn mit deinem Träwelling-Passwort erneut,
+ um einen neuen Token zu erhalten.</p>
+ </div>
+ <div class="card-action">
+ %= form_for '/account/traewelling' => (method => 'POST') => begin
+ %= csrf_field
+ <button class="btn-flat waves-effect waves-light white-text" type="submit" name="action" value="logout">
+ <i class="material-icons left" aria-hidden="true">sync_disabled</i>
+ Abmelden
+ </button>
+ %= end
+ </div>
+ </div>
+ </div>
+ </div>
+% }
+
+% if (not $traewelling->{token}) {
+ <div class="row">
+ <div class="col s12">
+ <p>
+ Hier hast du die Möglichkeit, deinen travelynx-Account mit einem
+ Account bei <a href="https://traewelling.de">Träwelling</a> zu
+ verknüpfen. Dies erlaubt die automatische Übernahme zukünftiger
+ Checkins zwischen den beiden Diensten. Checkins, die
+ vor dem Verknüpfen der Accounts stattgefunden haben, werden
+ nicht synchronisiert. Bei synchronisierten Checkins wird der
+ zugehörige Träwelling-Status von deiner travelynx-Statusseite
+ aus verlinkt.
+ </p>
+ </div>
+ </div>
+ <div class="row">
+ %= form_for '/oauth/traewelling' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="col s12 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="connect">
+ Verknüpfen
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ %= end
+ </div>
+% }
+% else {
+ <div class="row">
+ <div class="col s12">
+ <p>
+ Dieser travelynx-Account ist mit dem Träwelling-Account
+ % if (my $user = $traewelling->{data}{user_name}) {
+ <a href="https://traewelling.de/@<%= $user %>"><%= $user %></a>
+ % }
+ % else {
+ %= $traewelling->{email}
+ % }
+ verknüpft. Der aktuelle Token läuft <%= $traewelling->{expires_on}->strftime('am %d.%m.%Y um %H:%M Uhr') %> ab.
+ </p>
+ </div>
+ </div>
+ %= form_for '/account/traewelling' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button sync_source => 'none'
+ <span>Keine Synchronisierung</span>
+ </label>
+ </div>
+ </div>
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button sync_source => 'travelynx'
+ <span>Checkin-Synchronisierung travelynx → Träwelling</span>
+ </label>
+ </div>
+ <div>
+ <label>
+ %= check_box toot => 1
+ <span>… Checkin auf Mastodon veröffentlichen</span>
+ </label>
+ </div>
+ <div>
+ <label>
+ %= check_box tweet => 1
+ <span>… Checkin auf Twitter veröffentlichen</span>
+ </label>
+ </div>
+ <p>Die Synchronisierung erfolgt spätestens drei Minuten nach der
+ Zielwahl. Beachte, dass die Synchronisierung travelynx
+ → Träwelling unabhängig von der eingestellten Sichtbarkeit
+ des Checkins erfolgt. travelynx reicht die Sichtbarkeit
+ aber an Träwelling weiter.
+ Träwelling-Checkins können von travelynx aktuell nicht
+ rückgängig gemacht werden. Eine nachträgliche Änderung der
+ Zielstation wird nicht übernommen. Mastodon und Twitter beziehen
+ sich auf die in den <a
+ href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
+ verknüpften Accounts.</p>
+ </div>
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button sync_source => 'traewelling'
+ <span>Checkin-Synchronisierung Träwelling → travelynx</span>
+ </label>
+ </div>
+ <p>Alle fünf Minuten wird dein Status auf Träwelling abgefragt.
+ Falls du gerade in eingecheckt bist, wird der Checkin von
+ travelynx übernommen. Träwelling-Checkins in Züge
+ außerhalb des deutschen Schienennetzes werden noch nicht
+ unterstützt. Die Sichtbarkeit von Träwelling-Checkins wird
+ derzeit von travelynx nicht berücksichtigt.</p>
+ </div>
+ </div>
+ <div class="row hide-on-small-only">
+ <div class="col s12 m6 l6 center-align">
+ <button class="btn waves-effect waves-light red" type="submit" name="action" value="logout">
+ Abmelden
+ <i class="material-icons right" aria-hidden="true">sync_disabled</i>
+ </button>
+ </div>
+ <div class="col s12 m6 l6 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="config">
+ Speichern
+ <i class="material-icons right" aria-hidden="true">send</i>
+ </button>
+ </div>
+ </div>
+ <div class="row hide-on-med-and-up">
+ <div class="col s12 m6 l6 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="config">
+ Speichern
+ <i class="material-icons right" aria-hidden="true">send</i>
+ </button>
+ </div>
+ <div class="col s12 m6 l6 center-align" style="margin-top: 1em;">
+ <button class="btn waves-effect waves-light red" type="submit" name="action" value="logout">
+ Abmelden
+ <i class="material-icons right" aria-hidden="true">sync_disabled</i>
+ </button>
+ </div>
+ </div>
+ %= end
+ <h2>Status</h2>
+ <div class="row">
+ <div class="col s12"">
+ % if ($traewelling->{latest_run}->epoch) {
+ Letzter Checkin <%= $traewelling->{latest_run}->strftime('am %d.%m.%Y um %H:%M:%S') %><br/>
+ % if ($traewelling->{errored}) {
+ <i class="material-icons left">error</i>
+ Fehler: <%= $traewelling->{data}{error} %>
+ % }
+ % }
+ % else {
+ Bisher wurde noch kein Checkin übernommen.
+ % }
+ </div>
+ </div>
+ <h2>Log</h2>
+ <div class="row">
+ <div class="col s12"">
+ <ul>
+ % for my $log_entry (@{$traewelling->{data}{log} // []}) {
+ <li>
+ <%= $log_entry->[0]->strftime('%d.%m.%Y %H:%M:%S') %> –
+ % if ($log_entry->[2]) {
+ Träwelling <a href="https://traewelling.de/status/<%= $log_entry->[2] %>">#<%= $log_entry->[2] %></a> –
+ % }
+ %= $log_entry->[1]
+ </li>
+ % }
+ </ul>
+ </div>
+ </div>
+% }
diff --git a/templates/use_external_links.html.ep b/templates/use_external_links.html.ep
new file mode 100644
index 0000000..d7bebd7
--- /dev/null
+++ b/templates/use_external_links.html.ep
@@ -0,0 +1,82 @@
+<h1>Externe Dienste</h1>
+<div class="row">
+ <div class="col s12">
+ <p>
+ Travelynx kann an geeigneten Stellen Links zu externen Diensten
+ (z.B. Abfahrstafeln oder Informationen zum gerade genutzten Zug)
+ einbinden. Hier lässt sich konfigurieren, welcher Dienst für welche
+ Art von Informationen genutzt wird.
+ <p/>
+ </div>
+</div>
+<h2>Abfahrtstafel</h2>
+%= form_for '/account/services' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="row">
+ <div class="col s12">
+ Angaben zu anderen an einer Station verkehrenden Verkehrsmitteln
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button stationboard => '0'
+ <span>Keine</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button stationboard => '1'
+ <span><a href="https://dbf.finalrewind.org/">DBF</a> (Schienenverkehr)</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button stationboard => '2'
+ <span><a href="https://bahn.expert/">bahn.expert</a> (Schienenverkehr)</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button stationboard => '3'
+ <span><a href="https://dbf.finalrewind.org/?hafas=1">DBF</a> (Nahverkehr)</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ <div>
+ <label>
+ %= radio_button stationboard => '4'
+ <span><a href="https://bahn.expert/regional">bahn.expert/regional</a> (Nahverkehr)</span>
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s3 m3 l3">
+ </div>
+ <div class="col s6 m6 l6 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
+ Speichern
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ <div class="col s3 m3 l3">
+ </div>
+ </div>
+%= end
diff --git a/templates/use_history.html.ep b/templates/use_history.html.ep
index e8e129f..9b76e98 100644
--- a/templates/use_history.html.ep
+++ b/templates/use_history.html.ep
@@ -4,8 +4,8 @@
<p>
Travelynx kann anhand deiner vergangenen Fahrten Verbindungen zum
Einchecken vorschlagen. Fährst zu z.B regelmäßig von Dortmund Hbf
- nach Essen Hbf, werden dir in Dortmund bevorzugt Züge angezeigt, die über
- Essen fahren. Bei Auswahl dieser wird nicht nur in den Zug eingecheckt,
+ nach Essen Hbf, werden dir in Dortmund bevorzugt Fahrten angezeigt, die
+ Essen passieren. Bei Auswahl dieser wird nicht nur in die Fahrt eingecheckt,
sondern auch direkt Essen Hbf als Ziel eingetragen.
<p/>
<!-- <p>
diff --git a/templates/user_status.html.ep b/templates/user_status.html.ep
index 78ef547..45fba54 100644
--- a/templates/user_status.html.ep
+++ b/templates/user_status.html.ep
@@ -1,5 +1,5 @@
<div class="row">
<div class="col s12 publicstatuscol" data-user="<%= $name %>">
- %= include '_public_status_card', name => $name, public_level => $public_level, journey => $journey
+ %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey
</div>
</div>
diff --git a/templates/webhooks.html.ep b/templates/webhooks.html.ep
index 930732d..280ba27 100644
--- a/templates/webhooks.html.ep
+++ b/templates/webhooks.html.ep
@@ -88,12 +88,13 @@
Gültige Werte für reason sind derzeit:
<ul>
<li><b>ping</b> (nach jeder gespeicherten Änderung in diesem Formular)</li>
- <li><b>checkin</b> (in einen Zug eingecheckt – Zielstation ist noch nicht bekannt)</li>
- <li><b>update</b> (eingecheckt und Ziel gewählt oder geändert)</li>
- <li><b>checkout</b> (aus einem Zug ausgecheckt)</li>
+ <li><b>checkin</b> (eingecheckt – Zielstation ist noch nicht bekannt)</li>
+ <li><b>update</b> (eingecheckt und Ziel/Kommentar/Sichtbarkeit geändert)</li>
+ <li><b>checkout</b> (ausgecheckt)</li>
<li><b>undo</b> (checkin oder checkout wurde rückgängig gemacht)</li>
</ul>
- Falls der Zug das Ziel bei der Zielwahl schon erreicht hat, wird ohne
+ Falls die Fahrt das Ziel bei der Zielwahl schon erreicht hat, wird ohne
<b>update</b> direkt ein <b>checkout</b> abgeschickt.
</p>
+ </div>
</div>
diff --git a/templates/year_in_review.html.ep b/templates/year_in_review.html.ep
new file mode 100644
index 0000000..0518dc1
--- /dev/null
+++ b/templates/year_in_review.html.ep
@@ -0,0 +1,169 @@
+<div class="row">
+ <div class="col s12 m12 l12">
+ <div class="carousel carousel-slider center">
+ <div class="carousel-item" href="#one">
+ <h2>Jahresrückblick <%= $year %></h2>
+ <p>
+ Du hast in diesem Jahr <strong><%= $stats->{num_trains} %> Fahrten</strong> von und zu <strong><%= $review->{num_stops} %> Betriebsstellen</strong> in travelynx erfasst.
+ % if ($stats->{num_trains} > 365) {
+ Das sind mehr als <strong><%= $review->{trains_per_day} %> Fahrten pro Tag</strong>!
+ % }
+ </p>
+ <p>
+ % if ($review->{traveling_min_total} > 525) {
+ Insgesamt hast du mindestens <strong><%= $review->{traveling_percentage_year} %> des Jahres</strong>
+ (<%= $review->{traveling_time_year} %>) unterwegs verbracht.
+ % }
+ % else {
+ Insgesamt hast du mindestens <strong><%= $review->{traveling_time_year} %></strong> unterwegs verbracht.
+ % }
+ </p>
+ <p>
+ Dabei hast du ca. <strong><%= $review->{km_route} %> km</strong> (Luftlinie: <%= $review->{km_beeline} %> km) zurückgelegt.
+ % if ($review->{km_circle} > 1) {
+ Das entspricht <strong><%= $review->{km_circle_h} %> Fahrten um die Erde</strong>.
+ % }
+ % elsif ($review->{km_diag} > 1) {
+ Das entspricht <strong><%= $review->{km_diag_h} %> Reisen zum Mittelpunkt der Erde und zurück</strong>.
+ % }
+ </p>
+ <p>
+ <em>Hier streichen</em> 🐈 <em>oder unten klicken für nächste Seite</em>
+ </p>
+ </div>
+ <div class="carousel-item" href="#two">
+ <h2>Eine typische Fahrt</h2>
+ <p>
+ % if ($review->{typical_stops_3} and $review->{typical_type_1}) {
+ … führte dich mit
+ % if ($review->{typical_type_1} eq 'S') {
+ einer <strong>S-Bahn</strong>
+ % }
+ % else {
+ einem <strong><%= $review->{typical_type_1} %></strong>
+ % }
+ durch das Dreieck <strong><%= join(' / ', @{$review->{typical_stops_3}}) %></strong>.
+ % }
+ % elsif ($review->{typical_stops_2}) {
+ … befand sich jederzeit auf deiner Pendelstrecke zwischen <strong><%= $review->{typical_stops_2}[0] %></strong> und <strong><%= $review->{typical_stops_2}[1] %></strong>.
+ % }
+ </p>
+ <p>
+ Im Mittel benötigte sie <strong><%= $review->{typical_time} %></strong> für eine Entfernung von ca. <strong><%= $review->{typical_km} %> km</strong> (<%= $review->{typical_kmh} %> km/h).
+ </p>
+ % if ($review->{typical_delay_dep} == 0 and $review->{typical_delay_arr} == 0) {
+ <p>Außerdem war sie <strong>komplett pünktlich</strong>. Beeindruckend!</p>
+ % }
+ % elsif ($review->{typical_delay_dep} > 0 and $review->{typical_delay_arr} > 0) {
+ <p>Sie fuhr <strong><%= $review->{typical_delay_dep_h} %></strong> zu spät
+ % if ($review->{typical_delay_arr} < $review->{typical_delay_dep}) {
+ ab, konnte aber einen Teil der Verspätung wieder herausholen.
+ Ihr Ziel erreichte sie nur noch <strong><%= $review->{typical_delay_arr_h} %></strong> später als vorgesehen.
+ % }
+ % elsif ($review->{typical_delay_arr} == $review->{typical_delay_dep}) {
+ ab und kam mit der gleichen Verspätung am Ziel an.
+ % }
+ % else {
+ ab und schlich mit <strong>+<%= $review->{typical_delay_arr} %></strong> ins Ziel.
+ % }
+ % }
+ </div>
+ <div class="carousel-item" href="#three">
+ <h2>High Scores</h2>
+ % if ($review->{longest_t_id}) {
+ <p><a href="/journey/<%= $review->{longest_t_id} %>">Längste Fahrt</a>:
+ <strong><%= $review->{longest_t_time} %></strong> mit <strong><%= $review->{longest_t_type} %> <%= $review->{longest_t_lineno} %></strong> von <%= $review->{longest_t_from} %> nach <%= $review->{longest_t_to} %>.</p>
+ % if ($review->{longest_km_id} == $review->{longest_t_id}) {
+ <p>Mit <strong><%= $review->{longest_km_km} %> km</strong> war sie gleichzeitig deine weiteste Fahrt.</p>
+ % }
+ % }
+ % if ($review->{longest_km_id} and $review->{longest_km_id} != $review->{longest_t_id}) {
+ <p><a href="/journey/<%= $review->{longest_km_id} %>">Größte Entfernung</a>:
+ <strong><%= $review->{longest_km_km} %> km</strong> mit <strong><%= $review->{longest_km_type} %> <%= $review->{longest_km_lineno} %></strong> von <%= $review->{longest_km_from} %> nach <%= $review->{longest_km_to} %>.</p>
+ % }
+ % if ($review->{shortest_t_id}) {
+ <p><a href="/journey/<%= $review->{shortest_t_id} %>">Kürzeste Fahrt</a>:
+ <strong><%= $review->{shortest_t_time} %></strong> mit <strong><%= $review->{shortest_t_type} %> <%= $review->{shortest_t_lineno} %></strong> von <%= $review->{shortest_t_from} %> nach <%= $review->{shortest_t_to} %>.</p>
+ % if ($review->{shortest_km_id} == $review->{shortest_t_id}) {
+ <p>Mit <strong><%= $review->{shortest_km_m} %> m</strong> war sie gleichzeitig dein kleinster Katzensprung.</p>
+ % }
+ % }
+ % if ($review->{shortest_km_id} and $review->{shortest_km_id} != $review->{shortest_t_id}) {
+ <p><a href="/journey/<%= $review->{shortest_km_id} %>">Kleinster Katzensprung</a>:
+ <strong><%= $review->{shortest_km_m} %> m</strong> mit <strong><%= $review->{shortest_km_type} %> <%= $review->{shortest_km_lineno} %></strong> von <%= $review->{shortest_km_from} %> nach <%= $review->{shortest_km_to} %>.</p>
+ % }
+ </div>
+ <div class="carousel-item" href="#four">
+ <h2>Oepsie Woepsie</h2>
+ % if ($review->{issue1_count}) {
+ <p><strong><%= $review->{issue_percent} %></strong> aller Fahrten liefen nicht wie vorgesehen ab.<br/>
+ Die häufigsten Anmerkungen waren:</p>
+ % for my $i (1 .. 3) {
+ % if ($review->{"issue${i}_count"}) {
+ <p><strong><%= $review->{"issue${i}_count"} %>×</strong> „<%= $review->{"issue${i}_text"} %>“</p>
+ % }
+ % }
+ % }
+ <p>Lediglich <strong><%= $review->{punctual_percent_h} %></strong> der Fahrten waren pünktlich auf die Minute.</p>
+ </div>
+ <div class="carousel-item" href="#five">
+ <h2>De trein is stukkie wukkie</h2>
+ <p>
+ % if ($review->{fgr_percent} >= 0.1) {
+ <strong><%= $review->{fgr_percent_h} %></strong> deiner Fahrten hatten mindestens eine Stunde Verspätung
+ % }
+ % if ($review->{cancel_count}) {
+ % if ($review->{fgr_percent} >= 0.1) {
+ und <strong><%= $review->{cancel_count} %></strong> kamen gar nicht erst am Ziel an.
+ % }
+ % else {
+ <strong><%= $review->{cancel_count} %></strong> deiner geplanten Fahrten sind ausgefallen.
+ % }
+ % }
+ </p>
+ % if ($review->{most_delayed_id}) {
+ <p>
+ Mit <strong><%= $review->{most_delayed_delay_arr} %></strong> hatte <a href="/journey/<%= $review->{most_delayed_id} %>"><%= $review->{most_delayed_type} %> <%= $review->{most_delayed_lineno} %></a> <%= $review->{most_delayed_from} %> → <%= $review->{most_delayed_to} %> die größte Verspätung.
+ </p>
+ % }
+ % if ($review->{most_delay_id}) {
+ <p>
+ Die Fahrt mit <a href="/journey/<%= $review->{most_delay_id} %>"><%= $review->{most_delay_type} %> <%= $review->{most_delay_lineno} %></a>
+ von <%= $review->{most_delay_from} %> nach <%= $review->{most_delay_to} %> verlief besonders gemächlich:
+ sie dauerte <strong><%= $review->{most_delay_delta} %></strong> länger als geplant.
+ </p>
+ % }
+ % if ($review->{most_undelay_id}) {
+ <p>
+ In <a href="/journey/<%= $review->{most_undelay_id} %>"><%= $review->{most_undelay_type} %> <%= $review->{most_undelay_lineno} %></a>
+ wurde hingegen Vmax ausgereizt und die Strecke von
+ <%= $review->{most_undelay_from} %> nach <%= $review->{most_undelay_to} %>
+ <strong><%= $review->{most_undelay_delta} %></strong> schneller absolviert als vorgesehen.
+ </p>
+ % }
+ </div>
+ <div class="carousel-item" href="#six">
+ <h2>Last, but not least</h2>
+ % if ($review->{top_trip_count}) {
+ <p>
+ <strong><%= $review->{top_trip_percent_h} %></strong> deiner Check-Ins konzentrierten sich auf diese Strecken:<br/>
+ % for my $i (0 .. $#{$review->{top_trips}}) {
+ % my $trip = $review->{top_trips}[$i];
+ <%= join(q{ }, @{$trip}) %><br/>
+ % }
+ </p>
+ % }
+ % if ($review->{single_trip_count}) {
+ <p>
+ <a href="/history/<%= $year %>?filter=single"><strong><%= $review->{single_trip_percent_h} %></strong> aller Verbindungen</a> bist du nur genau <strong>einmal</strong> gefahren. Zum Beispiel:<br/>
+ % for my $i (0 .. $#{$review->{single_trips}}) {
+ % my $trip = $review->{single_trips}[$i];
+ <%= $trip->[0] %> → <%= $trip->[1] %><br/>
+ % }
+ </p>
+ % }
+ <p><em>Thank you for traveling with travelynx</em></p>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..d78f8a5
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Updates a travelynx instance deployed via git. Performs database migrations
+# as necessary.
+
+git pull
+
+if [ "$1" = "with-deps" ]; then
+ mkdir local.new
+ cd local.new
+ cp ../cpanfile* .
+ carton install
+ cd ..
+ sudo systemctl stop travelynx
+ mv local local.old
+ mv local.new/local .
+ perl index.pl database migrate
+ sudo systemctl start travelynx
+elif perl index.pl database has-current-schema; then
+ sudo systemctl reload travelynx
+else
+ sudo systemctl stop travelynx
+ perl index.pl database migrate
+ sudo systemctl start travelynx
+fi