summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/perl.yml2
-rw-r--r--.mailmap1
-rw-r--r--.reuse/dep518
-rw-r--r--Dockerfile35
-rw-r--r--README.md82
-rwxr-xr-xcontrib/i3bar-snippet.py111
-rw-r--r--contrib/polybar.sh201
-rw-r--r--cpanfile12
-rw-r--r--cpanfile.snapshot2303
-rwxr-xr-xdocker-run.sh62
-rw-r--r--examples/docker/email-transport.sh5
-rw-r--r--examples/docker/travelynx.conf26
-rw-r--r--examples/travelynx.conf64
-rw-r--r--index.pl2
-rwxr-xr-xlib/Travelynx.pm2340
-rw-r--r--lib/Travelynx/Command/account.pm119
-rw-r--r--lib/Travelynx/Command/database.pm1179
-rw-r--r--lib/Travelynx/Command/dumpconfig.pm2
-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.pm169
-rw-r--r--lib/Travelynx/Command/munin.pm8
-rw-r--r--lib/Travelynx/Command/traewelling.pm207
-rw-r--r--lib/Travelynx/Command/work.pm332
-rw-r--r--lib/Travelynx/Command/worker.pm26
-rw-r--r--lib/Travelynx/Controller/Account.pm1008
-rwxr-xr-xlib/Travelynx/Controller/Api.pm315
-rw-r--r--lib/Travelynx/Controller/Passengerrights.pm10
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm576
-rw-r--r--lib/Travelynx/Controller/Static.pm19
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm117
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1829
-rw-r--r--lib/Travelynx/Helper/DBDB.pm36
-rw-r--r--lib/Travelynx/Helper/HAFAS.pm388
-rw-r--r--lib/Travelynx/Helper/IRIS.pm145
-rw-r--r--lib/Travelynx/Helper/Sendmail.pm33
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm235
-rw-r--r--lib/Travelynx/Model/InTransit.pm651
-rwxr-xr-xlib/Travelynx/Model/JourneyStatsCache.pm2
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm863
-rw-r--r--lib/Travelynx/Model/Stations.pm199
-rw-r--r--lib/Travelynx/Model/Traewelling.pm51
-rw-r--r--lib/Travelynx/Model/Users.pm756
-rw-r--r--public/service-worker.js25
-rw-r--r--public/static/api.yml2
-rw-r--r--public/static/css/dark.min.css4
-rw-r--r--public/static/css/light.min.css4
-rw-r--r--public/static/css/local.css65
-rw-r--r--public/static/css/material-icons.css8
-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.js8836
-rw-r--r--public/static/js/autocomplete.min.js1
-rw-r--r--public/static/js/geolocation.js63
-rw-r--r--public/static/js/geolocation.min.js2
-rw-r--r--public/static/js/travelynx-actions.js86
-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/v37)0
l---------public/static/v72 (renamed from public/static/v38)0
-rw-r--r--sass/components/_carousel.scss4
-rw-r--r--sass/components/_variables.scss1
-rw-r--r--sass/components/forms/_forms.scss2
-rw-r--r--sass/components/forms/_input-fields.scss2
-rw-r--r--sass/src/common/index.scss31
-rw-r--r--sass/src/common/local.scss297
-rw-r--r--sass/src/dark/_variables.scss5
-rw-r--r--sass/src/dark/index.scss1
-rw-r--r--sass/src/light/_variables.scss3
-rw-r--r--sass/src/light/index.scss1
-rwxr-xr-xscripts/asset-rebuild6
-rwxr-xr-xscripts/asset-release2
-rwxr-xr-xscripts/update-autocomplete42
-rwxr-xr-xshare/ice_names.json483
-rwxr-xr-xshare/old_station_names.json224
-rw-r--r--share/old_stations.json2603
-rw-r--r--t/01-static.t2
-rw-r--r--t/02-registration.t9
-rw-r--r--t/11-journey-stats.t3
-rw-r--r--t/12-journey-edit.t3
-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.t3
-rw-r--r--templates/_cancelled.html.ep27
-rw-r--r--templates/_cancelled_departure.html.ep6
-rw-r--r--templates/_checked_in.html.ep290
-rw-r--r--templates/_checked_out.html.ep18
-rw-r--r--templates/_connections.html.ep142
-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_stats.html.ep37
-rw-r--r--templates/_history_trains.html.ep90
-rw-r--r--templates/_invalid_input.html.ep9
-rw-r--r--templates/_map.html.ep2
-rw-r--r--templates/_public_status_card.html.ep128
-rw-r--r--templates/_timeline-checked-in.html.ep14
-rw-r--r--templates/_timeline_link.html.ep16
-rw-r--r--templates/about.html.ep10
-rw-r--r--templates/account.html.ep211
-rw-r--r--templates/add_journey.html.ep14
-rw-r--r--templates/api_documentation.html.ep71
-rw-r--r--templates/bad_request.html.ep19
-rw-r--r--templates/change_password.html.ep4
-rw-r--r--templates/changelog.html.ep402
-rw-r--r--templates/commute.html.ep4
-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.ep6
-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.ep2
-rw-r--r--templates/history_by_year.html.ep27
-rw-r--r--templates/history_map.html.ep97
-rw-r--r--templates/journey.html.ep67
-rw-r--r--templates/landingpage.html.ep41
-rw-r--r--templates/layouts/default.html.ep74
-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.ep176
-rw-r--r--templates/profile.html.ep81
-rw-r--r--templates/register.html.ep20
-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.ep112
-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.ep8
-rw-r--r--templates/year_in_review.html.ep169
150 files changed, 19180 insertions, 14435 deletions
diff --git a/.github/workflows/perl.yml b/.github/workflows/perl.yml
index 64f8a15..2e64b35 100644
--- a/.github/workflows/perl.yml
+++ b/.github/workflows/perl.yml
@@ -45,6 +45,6 @@ jobs:
- name: Install PostgreSQL Client Library
run: apt install libpq-dev
- name: Install Perl Dependencies
- run: curl -sL https://git.io/cpm | perl - install -g --show-build-log-on-failure
+ 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
index 65773c1..b152498 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -1,15 +1,15 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files: .dockerignore .github/* .github/*/* .gitignore
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: CC0-1.0
Files: Dockerfile README.md cpanfile cpanfile.snapshot
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: CC0-1.0
Files: examples/*
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: CC0-1.0
Files: public/static/css/material-icons.css public/static/fonts/MaterialIcons-*
@@ -26,11 +26,11 @@ License: Apache-2.0
Files: public/static/js/autocomplete.min.js
Copyright: 2020 DB Station&Service AG, Europaplatz 1, 10557 Berlin
- 2020 Daniel Friesel
+ 2020 Birte Kristina Friesel
License: CC-BY-4.0
Files: public/static/js/geolocation.min.js public/static/js/travelynx-actions.js
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: MIT
Files: public/static/js/jquery-3.4.1.min.js
@@ -47,11 +47,11 @@ Copyright: 2010-2019 Vladimir Agafonkin
License: BSD-2-Clause
Files: public/static/manifest.json
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: CC0-1.0
Files: templates/*
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: MIT
Files: sass/materialize.scss sass/components/*
@@ -60,7 +60,7 @@ License: MIT
Files: sass/src/*
Copyright: 2019 marudor
- 2020 Daniel Friesel
+ 2020 Birte Kristina Friesel
License: MIT
Files: share/ice_names.json
@@ -68,5 +68,5 @@ Copyright: 2017 marudor
License: MIT
Files: share/old_station_names.json
-Copyright: 2020 Daniel Friesel
+Copyright: 2020 Birte Kristina Friesel
License: CC0-1.0
diff --git a/Dockerfile b/Dockerfile
index d916c6c..316d274 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,27 @@
-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/
WORKDIR /app
@@ -16,8 +37,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
libpq-dev \
libssl1.1 \
libssl-dev \
- libxml2 \
- libxml2-dev \
make \
zlib1g-dev \
&& cpanm -in --no-man-pages --installdeps . \
@@ -29,11 +48,13 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
libc6-dev \
libdb5.3-dev \
libssl-dev \
- libxml2-dev \
make \
zlib1g-dev \
- && apt-get autoremove -y
+ && 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/README.md b/README.md
index 4a9435d..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.20
- * 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.
-
-In the project root directory (where `cpanfile` resides), run either
-
-```
-carton install
-```
+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.
-or
+In the project root directory (where `cpanfile` resides), run
```
-cpanm --installdeps .
+carton install --deployment
```
and set `PERL5LIB=.../local/lib/perl5` before executing any travelynx
@@ -88,6 +88,7 @@ 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
@@ -100,9 +101,47 @@ fi
Note that this is subject to change -- the application may perform schema
updates automatically in the future. If you used carton for installation,
-use `carton exec perl ...` in the snippet above; if you used cpanm, export
+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
---
@@ -173,3 +212,12 @@ both for personal/internal and public use, under the following conditions.
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 f7b80f5..f81887c 100644
--- a/cpanfile
+++ b/cpanfile
@@ -4,17 +4,19 @@ requires 'Crypt::Eksblowfish';
requires 'DateTime';
requires 'DateTime::Format::Strptime';
requires 'Email::Sender::Simple';
-requires 'Geo::Distance';
-requires 'Geo::Distance::XS';
+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 'Text::CSV';
-requires 'Travel::Status::DE::DBWagenreihung';
-requires 'Travel::Status::DE::IRIS', '1.60';
+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 5a59dc5..2318de5 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -1,82 +1,94 @@
# carton snapshot format: version 1.0
DISTRIBUTIONS
- Alien-Build-2.40
- pathname: P/PL/PLICEASE/Alien-Build-2.40.tar.gz
- provides:
- Alien::Base 2.40
- Alien::Base::PkgConfig 2.40
- Alien::Base::Wrapper 2.40
- Alien::Build 2.40
- Alien::Build::CommandSequence 2.40
- Alien::Build::Helper 2.40
- Alien::Build::Interpolate 2.40
- Alien::Build::Interpolate::Default 2.40
- Alien::Build::Interpolate::Helper 2.40
- Alien::Build::Log 2.40
- Alien::Build::Log::Abbreviate 2.40
- Alien::Build::Log::Default 2.40
- Alien::Build::MM 2.40
- Alien::Build::Meta 2.40
- Alien::Build::Plugin 2.40
- Alien::Build::Plugin::Build::Autoconf 2.40
- Alien::Build::Plugin::Build::CMake 2.40
- Alien::Build::Plugin::Build::Copy 2.40
- Alien::Build::Plugin::Build::MSYS 2.40
- Alien::Build::Plugin::Build::Make 2.40
- Alien::Build::Plugin::Build::SearchDep 2.40
- Alien::Build::Plugin::Core::CleanInstall 2.40
- Alien::Build::Plugin::Core::Download 2.40
- Alien::Build::Plugin::Core::FFI 2.40
- Alien::Build::Plugin::Core::Gather 2.40
- Alien::Build::Plugin::Core::Legacy 2.40
- Alien::Build::Plugin::Core::Override 2.40
- Alien::Build::Plugin::Core::Setup 2.40
- Alien::Build::Plugin::Core::Tail 2.40
- Alien::Build::Plugin::Decode::DirListing 2.40
- Alien::Build::Plugin::Decode::DirListingFtpcopy 2.40
- Alien::Build::Plugin::Decode::HTML 2.40
- Alien::Build::Plugin::Decode::Mojo 2.40
- Alien::Build::Plugin::Download::Negotiate 2.40
- Alien::Build::Plugin::Extract::ArchiveTar 2.40
- Alien::Build::Plugin::Extract::ArchiveZip 2.40
- Alien::Build::Plugin::Extract::CommandLine 2.40
- Alien::Build::Plugin::Extract::Directory 2.40
- Alien::Build::Plugin::Extract::Negotiate 2.40
- Alien::Build::Plugin::Fetch::CurlCommand 2.40
- Alien::Build::Plugin::Fetch::HTTPTiny 2.40
- Alien::Build::Plugin::Fetch::LWP 2.40
- Alien::Build::Plugin::Fetch::Local 2.40
- Alien::Build::Plugin::Fetch::LocalDir 2.40
- Alien::Build::Plugin::Fetch::NetFTP 2.40
- Alien::Build::Plugin::Fetch::Wget 2.40
- Alien::Build::Plugin::Gather::IsolateDynamic 2.40
- Alien::Build::Plugin::PkgConfig::CommandLine 2.40
- Alien::Build::Plugin::PkgConfig::LibPkgConf 2.40
- Alien::Build::Plugin::PkgConfig::MakeStatic 2.40
- Alien::Build::Plugin::PkgConfig::Negotiate 2.40
- Alien::Build::Plugin::PkgConfig::PP 2.40
- Alien::Build::Plugin::Prefer::BadVersion 2.40
- Alien::Build::Plugin::Prefer::GoodVersion 2.40
- Alien::Build::Plugin::Prefer::SortVersions 2.40
- Alien::Build::Plugin::Probe::CBuilder 2.40
- Alien::Build::Plugin::Probe::CommandLine 2.40
- Alien::Build::Plugin::Probe::Vcpkg 2.40
- Alien::Build::Plugin::Test::Mock 2.40
- Alien::Build::PluginMeta 2.40
- Alien::Build::Temp 2.40
- Alien::Build::TempDir 2.40
- Alien::Build::Util 2.40
- Alien::Build::Version::Basic 2.40
- Alien::Build::rc 2.40
- Alien::Role 2.40
- Test::Alien 2.40
- Test::Alien::Build 2.40
- Test::Alien::CanCompile 2.40
- Test::Alien::CanPlatypus 2.40
- Test::Alien::Diag 2.40
- Test::Alien::Run 2.40
- Test::Alien::Synthetic 2.40
- alienfile 2.40
+ 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
@@ -92,27 +104,41 @@ DISTRIBUTIONS
PkgConfig 0.14026
Test2::API 1.302096
Text::ParseWords 3.26
+ parent 0
perl 5.008004
- Alien-Libxml2-0.17
- pathname: P/PL/PLICEASE/Alien-Libxml2-0.17.tar.gz
+ Alien-Build-Plugin-Download-GitLab-0.01
+ pathname: P/PL/PLICEASE/Alien-Build-Plugin-Download-GitLab-0.01.tar.gz
provides:
- Alien::Libxml2 0.17
+ Alien::Build::Plugin::Download::GitLab 0.01
+ requirements:
+ 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
Alien::Build::Plugin::Probe::Vcpkg 0
ExtUtils::CBuilder 0
ExtUtils::MakeMaker 6.52
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
@@ -224,10 +250,10 @@ 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.36
@@ -240,21 +266,21 @@ DISTRIBUTIONS
File::Spec 0.80
base 0
perl 5.008
- Class-Measure-0.09
- pathname: B/BL/BLUEFEET/Class-Measure-0.09.tar.gz
+ Class-Measure-0.10
+ pathname: B/BL/BLUEFEET/Class-Measure-0.10.tar.gz
provides:
- Class::Measure 0.09
- Class::Measure::Length 0.09
+ 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.13
- pathname: E/ET/ETHER/Class-Method-Modifiers-2.13.tar.gz
+ Class-Method-Modifiers-2.15
+ pathname: E/ET/ETHER/Class-Method-Modifiers-2.15.tar.gz
provides:
- Class::Method::Modifiers 2.13
+ Class::Method::Modifiers 2.15
requirements:
B 0
Carp 0
@@ -289,6 +315,12 @@ DISTRIBUTIONS
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
pathname: H/HE/HERMES/Clone-Choose-0.010.tar.gz
provides:
@@ -338,14 +370,15 @@ DISTRIBUTIONS
Crypt::RC4 2.02
requirements:
ExtUtils::MakeMaker 0
- DBD-Pg-3.15.0
- pathname: T/TU/TURNSTEP/DBD-Pg-3.15.0.tar.gz
+ DBD-Pg-3.18.0
+ pathname: T/TU/TURNSTEP/DBD-Pg-3.18.0.tar.gz
provides:
- Bundle::DBD::Pg v3.15.0
- DBD::Pg v3.15.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
@@ -451,30 +484,31 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 6.48
Test::Simple 0.90
perl 5.008001
- Data-OptList-0.110
- pathname: R/RJ/RJBS/Data-OptList-0.110.tar.gz
+ 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.54
- pathname: D/DR/DROLSKY/DateTime-1.54.tar.gz
- provides:
- DateTime 1.54
- DateTime::Duration 1.54
- DateTime::Helpers 1.54
- DateTime::Infinite 1.54
- DateTime::Infinite::Future 1.54
- DateTime::Infinite::Past 1.54
- DateTime::LeapSecond 1.54
- DateTime::PP 1.54
- DateTime::PPExtra 1.54
- DateTime::Types 1.54
+ 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
@@ -490,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
@@ -526,15 +560,15 @@ DISTRIBUTIONS
parent 0
strict 0
warnings 0
- DateTime-Locale-1.32
- pathname: D/DR/DROLSKY/DateTime-Locale-1.32.tar.gz
+ DateTime-Locale-1.41
+ pathname: D/DR/DROLSKY/DateTime-Locale-1.41.tar.gz
provides:
- DateTime::Locale 1.32
- DateTime::Locale::Base 1.32
- DateTime::Locale::Catalog 1.32
- DateTime::Locale::Data 1.32
- DateTime::Locale::FromData 1.32
- DateTime::Locale::Util 1.32
+ 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
@@ -552,381 +586,346 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
- DateTime-TimeZone-2.47
- pathname: D/DR/DROLSKY/DateTime-TimeZone-2.47.tar.gz
- provides:
- DateTime::TimeZone 2.47
- DateTime::TimeZone::Africa::Abidjan 2.47
- DateTime::TimeZone::Africa::Accra 2.47
- DateTime::TimeZone::Africa::Algiers 2.47
- DateTime::TimeZone::Africa::Bissau 2.47
- DateTime::TimeZone::Africa::Cairo 2.47
- DateTime::TimeZone::Africa::Casablanca 2.47
- DateTime::TimeZone::Africa::Ceuta 2.47
- DateTime::TimeZone::Africa::El_Aaiun 2.47
- DateTime::TimeZone::Africa::Johannesburg 2.47
- DateTime::TimeZone::Africa::Juba 2.47
- DateTime::TimeZone::Africa::Khartoum 2.47
- DateTime::TimeZone::Africa::Lagos 2.47
- DateTime::TimeZone::Africa::Maputo 2.47
- DateTime::TimeZone::Africa::Monrovia 2.47
- DateTime::TimeZone::Africa::Nairobi 2.47
- DateTime::TimeZone::Africa::Ndjamena 2.47
- DateTime::TimeZone::Africa::Sao_Tome 2.47
- DateTime::TimeZone::Africa::Tripoli 2.47
- DateTime::TimeZone::Africa::Tunis 2.47
- DateTime::TimeZone::Africa::Windhoek 2.47
- DateTime::TimeZone::America::Adak 2.47
- DateTime::TimeZone::America::Anchorage 2.47
- DateTime::TimeZone::America::Araguaina 2.47
- DateTime::TimeZone::America::Argentina::Buenos_Aires 2.47
- DateTime::TimeZone::America::Argentina::Catamarca 2.47
- DateTime::TimeZone::America::Argentina::Cordoba 2.47
- DateTime::TimeZone::America::Argentina::Jujuy 2.47
- DateTime::TimeZone::America::Argentina::La_Rioja 2.47
- DateTime::TimeZone::America::Argentina::Mendoza 2.47
- DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.47
- DateTime::TimeZone::America::Argentina::Salta 2.47
- DateTime::TimeZone::America::Argentina::San_Juan 2.47
- DateTime::TimeZone::America::Argentina::San_Luis 2.47
- DateTime::TimeZone::America::Argentina::Tucuman 2.47
- DateTime::TimeZone::America::Argentina::Ushuaia 2.47
- DateTime::TimeZone::America::Asuncion 2.47
- DateTime::TimeZone::America::Atikokan 2.47
- DateTime::TimeZone::America::Bahia 2.47
- DateTime::TimeZone::America::Bahia_Banderas 2.47
- DateTime::TimeZone::America::Barbados 2.47
- DateTime::TimeZone::America::Belem 2.47
- DateTime::TimeZone::America::Belize 2.47
- DateTime::TimeZone::America::Blanc_Sablon 2.47
- DateTime::TimeZone::America::Boa_Vista 2.47
- DateTime::TimeZone::America::Bogota 2.47
- DateTime::TimeZone::America::Boise 2.47
- DateTime::TimeZone::America::Cambridge_Bay 2.47
- DateTime::TimeZone::America::Campo_Grande 2.47
- DateTime::TimeZone::America::Cancun 2.47
- DateTime::TimeZone::America::Caracas 2.47
- DateTime::TimeZone::America::Cayenne 2.47
- DateTime::TimeZone::America::Chicago 2.47
- DateTime::TimeZone::America::Chihuahua 2.47
- DateTime::TimeZone::America::Costa_Rica 2.47
- DateTime::TimeZone::America::Creston 2.47
- DateTime::TimeZone::America::Cuiaba 2.47
- DateTime::TimeZone::America::Curacao 2.47
- DateTime::TimeZone::America::Danmarkshavn 2.47
- DateTime::TimeZone::America::Dawson 2.47
- DateTime::TimeZone::America::Dawson_Creek 2.47
- DateTime::TimeZone::America::Denver 2.47
- DateTime::TimeZone::America::Detroit 2.47
- DateTime::TimeZone::America::Edmonton 2.47
- DateTime::TimeZone::America::Eirunepe 2.47
- DateTime::TimeZone::America::El_Salvador 2.47
- DateTime::TimeZone::America::Fort_Nelson 2.47
- DateTime::TimeZone::America::Fortaleza 2.47
- DateTime::TimeZone::America::Glace_Bay 2.47
- DateTime::TimeZone::America::Goose_Bay 2.47
- DateTime::TimeZone::America::Grand_Turk 2.47
- DateTime::TimeZone::America::Guatemala 2.47
- DateTime::TimeZone::America::Guayaquil 2.47
- DateTime::TimeZone::America::Guyana 2.47
- DateTime::TimeZone::America::Halifax 2.47
- DateTime::TimeZone::America::Havana 2.47
- DateTime::TimeZone::America::Hermosillo 2.47
- DateTime::TimeZone::America::Indiana::Indianapolis 2.47
- DateTime::TimeZone::America::Indiana::Knox 2.47
- DateTime::TimeZone::America::Indiana::Marengo 2.47
- DateTime::TimeZone::America::Indiana::Petersburg 2.47
- DateTime::TimeZone::America::Indiana::Tell_City 2.47
- DateTime::TimeZone::America::Indiana::Vevay 2.47
- DateTime::TimeZone::America::Indiana::Vincennes 2.47
- DateTime::TimeZone::America::Indiana::Winamac 2.47
- DateTime::TimeZone::America::Inuvik 2.47
- DateTime::TimeZone::America::Iqaluit 2.47
- DateTime::TimeZone::America::Jamaica 2.47
- DateTime::TimeZone::America::Juneau 2.47
- DateTime::TimeZone::America::Kentucky::Louisville 2.47
- DateTime::TimeZone::America::Kentucky::Monticello 2.47
- DateTime::TimeZone::America::La_Paz 2.47
- DateTime::TimeZone::America::Lima 2.47
- DateTime::TimeZone::America::Los_Angeles 2.47
- DateTime::TimeZone::America::Maceio 2.47
- DateTime::TimeZone::America::Managua 2.47
- DateTime::TimeZone::America::Manaus 2.47
- DateTime::TimeZone::America::Martinique 2.47
- DateTime::TimeZone::America::Matamoros 2.47
- DateTime::TimeZone::America::Mazatlan 2.47
- DateTime::TimeZone::America::Menominee 2.47
- DateTime::TimeZone::America::Merida 2.47
- DateTime::TimeZone::America::Metlakatla 2.47
- DateTime::TimeZone::America::Mexico_City 2.47
- DateTime::TimeZone::America::Miquelon 2.47
- DateTime::TimeZone::America::Moncton 2.47
- DateTime::TimeZone::America::Monterrey 2.47
- DateTime::TimeZone::America::Montevideo 2.47
- DateTime::TimeZone::America::Nassau 2.47
- DateTime::TimeZone::America::New_York 2.47
- DateTime::TimeZone::America::Nipigon 2.47
- DateTime::TimeZone::America::Nome 2.47
- DateTime::TimeZone::America::Noronha 2.47
- DateTime::TimeZone::America::North_Dakota::Beulah 2.47
- DateTime::TimeZone::America::North_Dakota::Center 2.47
- DateTime::TimeZone::America::North_Dakota::New_Salem 2.47
- DateTime::TimeZone::America::Nuuk 2.47
- DateTime::TimeZone::America::Ojinaga 2.47
- DateTime::TimeZone::America::Panama 2.47
- DateTime::TimeZone::America::Pangnirtung 2.47
- DateTime::TimeZone::America::Paramaribo 2.47
- DateTime::TimeZone::America::Phoenix 2.47
- DateTime::TimeZone::America::Port_au_Prince 2.47
- DateTime::TimeZone::America::Port_of_Spain 2.47
- DateTime::TimeZone::America::Porto_Velho 2.47
- DateTime::TimeZone::America::Puerto_Rico 2.47
- DateTime::TimeZone::America::Punta_Arenas 2.47
- DateTime::TimeZone::America::Rainy_River 2.47
- DateTime::TimeZone::America::Rankin_Inlet 2.47
- DateTime::TimeZone::America::Recife 2.47
- DateTime::TimeZone::America::Regina 2.47
- DateTime::TimeZone::America::Resolute 2.47
- DateTime::TimeZone::America::Rio_Branco 2.47
- DateTime::TimeZone::America::Santarem 2.47
- DateTime::TimeZone::America::Santiago 2.47
- DateTime::TimeZone::America::Santo_Domingo 2.47
- DateTime::TimeZone::America::Sao_Paulo 2.47
- DateTime::TimeZone::America::Scoresbysund 2.47
- DateTime::TimeZone::America::Sitka 2.47
- DateTime::TimeZone::America::St_Johns 2.47
- DateTime::TimeZone::America::Swift_Current 2.47
- DateTime::TimeZone::America::Tegucigalpa 2.47
- DateTime::TimeZone::America::Thule 2.47
- DateTime::TimeZone::America::Thunder_Bay 2.47
- DateTime::TimeZone::America::Tijuana 2.47
- DateTime::TimeZone::America::Toronto 2.47
- DateTime::TimeZone::America::Vancouver 2.47
- DateTime::TimeZone::America::Whitehorse 2.47
- DateTime::TimeZone::America::Winnipeg 2.47
- DateTime::TimeZone::America::Yakutat 2.47
- DateTime::TimeZone::America::Yellowknife 2.47
- DateTime::TimeZone::Antarctica::Casey 2.47
- DateTime::TimeZone::Antarctica::Davis 2.47
- DateTime::TimeZone::Antarctica::DumontDUrville 2.47
- DateTime::TimeZone::Antarctica::Macquarie 2.47
- DateTime::TimeZone::Antarctica::Mawson 2.47
- DateTime::TimeZone::Antarctica::Palmer 2.47
- DateTime::TimeZone::Antarctica::Rothera 2.47
- DateTime::TimeZone::Antarctica::Syowa 2.47
- DateTime::TimeZone::Antarctica::Troll 2.47
- DateTime::TimeZone::Antarctica::Vostok 2.47
- DateTime::TimeZone::Asia::Almaty 2.47
- DateTime::TimeZone::Asia::Amman 2.47
- DateTime::TimeZone::Asia::Anadyr 2.47
- DateTime::TimeZone::Asia::Aqtau 2.47
- DateTime::TimeZone::Asia::Aqtobe 2.47
- DateTime::TimeZone::Asia::Ashgabat 2.47
- DateTime::TimeZone::Asia::Atyrau 2.47
- DateTime::TimeZone::Asia::Baghdad 2.47
- DateTime::TimeZone::Asia::Baku 2.47
- DateTime::TimeZone::Asia::Bangkok 2.47
- DateTime::TimeZone::Asia::Barnaul 2.47
- DateTime::TimeZone::Asia::Beirut 2.47
- DateTime::TimeZone::Asia::Bishkek 2.47
- DateTime::TimeZone::Asia::Brunei 2.47
- DateTime::TimeZone::Asia::Chita 2.47
- DateTime::TimeZone::Asia::Choibalsan 2.47
- DateTime::TimeZone::Asia::Colombo 2.47
- DateTime::TimeZone::Asia::Damascus 2.47
- DateTime::TimeZone::Asia::Dhaka 2.47
- DateTime::TimeZone::Asia::Dili 2.47
- DateTime::TimeZone::Asia::Dubai 2.47
- DateTime::TimeZone::Asia::Dushanbe 2.47
- DateTime::TimeZone::Asia::Famagusta 2.47
- DateTime::TimeZone::Asia::Gaza 2.47
- DateTime::TimeZone::Asia::Hebron 2.47
- DateTime::TimeZone::Asia::Ho_Chi_Minh 2.47
- DateTime::TimeZone::Asia::Hong_Kong 2.47
- DateTime::TimeZone::Asia::Hovd 2.47
- DateTime::TimeZone::Asia::Irkutsk 2.47
- DateTime::TimeZone::Asia::Jakarta 2.47
- DateTime::TimeZone::Asia::Jayapura 2.47
- DateTime::TimeZone::Asia::Jerusalem 2.47
- DateTime::TimeZone::Asia::Kabul 2.47
- DateTime::TimeZone::Asia::Kamchatka 2.47
- DateTime::TimeZone::Asia::Karachi 2.47
- DateTime::TimeZone::Asia::Kathmandu 2.47
- DateTime::TimeZone::Asia::Khandyga 2.47
- DateTime::TimeZone::Asia::Kolkata 2.47
- DateTime::TimeZone::Asia::Krasnoyarsk 2.47
- DateTime::TimeZone::Asia::Kuala_Lumpur 2.47
- DateTime::TimeZone::Asia::Kuching 2.47
- DateTime::TimeZone::Asia::Macau 2.47
- DateTime::TimeZone::Asia::Magadan 2.47
- DateTime::TimeZone::Asia::Makassar 2.47
- DateTime::TimeZone::Asia::Manila 2.47
- DateTime::TimeZone::Asia::Nicosia 2.47
- DateTime::TimeZone::Asia::Novokuznetsk 2.47
- DateTime::TimeZone::Asia::Novosibirsk 2.47
- DateTime::TimeZone::Asia::Omsk 2.47
- DateTime::TimeZone::Asia::Oral 2.47
- DateTime::TimeZone::Asia::Pontianak 2.47
- DateTime::TimeZone::Asia::Pyongyang 2.47
- DateTime::TimeZone::Asia::Qatar 2.47
- DateTime::TimeZone::Asia::Qostanay 2.47
- DateTime::TimeZone::Asia::Qyzylorda 2.47
- DateTime::TimeZone::Asia::Riyadh 2.47
- DateTime::TimeZone::Asia::Sakhalin 2.47
- DateTime::TimeZone::Asia::Samarkand 2.47
- DateTime::TimeZone::Asia::Seoul 2.47
- DateTime::TimeZone::Asia::Shanghai 2.47
- DateTime::TimeZone::Asia::Singapore 2.47
- DateTime::TimeZone::Asia::Srednekolymsk 2.47
- DateTime::TimeZone::Asia::Taipei 2.47
- DateTime::TimeZone::Asia::Tashkent 2.47
- DateTime::TimeZone::Asia::Tbilisi 2.47
- DateTime::TimeZone::Asia::Tehran 2.47
- DateTime::TimeZone::Asia::Thimphu 2.47
- DateTime::TimeZone::Asia::Tokyo 2.47
- DateTime::TimeZone::Asia::Tomsk 2.47
- DateTime::TimeZone::Asia::Ulaanbaatar 2.47
- DateTime::TimeZone::Asia::Urumqi 2.47
- DateTime::TimeZone::Asia::Ust_Nera 2.47
- DateTime::TimeZone::Asia::Vladivostok 2.47
- DateTime::TimeZone::Asia::Yakutsk 2.47
- DateTime::TimeZone::Asia::Yangon 2.47
- DateTime::TimeZone::Asia::Yekaterinburg 2.47
- DateTime::TimeZone::Asia::Yerevan 2.47
- DateTime::TimeZone::Atlantic::Azores 2.47
- DateTime::TimeZone::Atlantic::Bermuda 2.47
- DateTime::TimeZone::Atlantic::Canary 2.47
- DateTime::TimeZone::Atlantic::Cape_Verde 2.47
- DateTime::TimeZone::Atlantic::Faroe 2.47
- DateTime::TimeZone::Atlantic::Madeira 2.47
- DateTime::TimeZone::Atlantic::Reykjavik 2.47
- DateTime::TimeZone::Atlantic::South_Georgia 2.47
- DateTime::TimeZone::Atlantic::Stanley 2.47
- DateTime::TimeZone::Australia::Adelaide 2.47
- DateTime::TimeZone::Australia::Brisbane 2.47
- DateTime::TimeZone::Australia::Broken_Hill 2.47
- DateTime::TimeZone::Australia::Darwin 2.47
- DateTime::TimeZone::Australia::Eucla 2.47
- DateTime::TimeZone::Australia::Hobart 2.47
- DateTime::TimeZone::Australia::Lindeman 2.47
- DateTime::TimeZone::Australia::Lord_Howe 2.47
- DateTime::TimeZone::Australia::Melbourne 2.47
- DateTime::TimeZone::Australia::Perth 2.47
- DateTime::TimeZone::Australia::Sydney 2.47
- DateTime::TimeZone::CET 2.47
- DateTime::TimeZone::CST6CDT 2.47
- DateTime::TimeZone::Catalog 2.47
- DateTime::TimeZone::EET 2.47
- DateTime::TimeZone::EST 2.47
- DateTime::TimeZone::EST5EDT 2.47
- DateTime::TimeZone::Europe::Amsterdam 2.47
- DateTime::TimeZone::Europe::Andorra 2.47
- DateTime::TimeZone::Europe::Astrakhan 2.47
- DateTime::TimeZone::Europe::Athens 2.47
- DateTime::TimeZone::Europe::Belgrade 2.47
- DateTime::TimeZone::Europe::Berlin 2.47
- DateTime::TimeZone::Europe::Brussels 2.47
- DateTime::TimeZone::Europe::Bucharest 2.47
- DateTime::TimeZone::Europe::Budapest 2.47
- DateTime::TimeZone::Europe::Chisinau 2.47
- DateTime::TimeZone::Europe::Copenhagen 2.47
- DateTime::TimeZone::Europe::Dublin 2.47
- DateTime::TimeZone::Europe::Gibraltar 2.47
- DateTime::TimeZone::Europe::Helsinki 2.47
- DateTime::TimeZone::Europe::Istanbul 2.47
- DateTime::TimeZone::Europe::Kaliningrad 2.47
- DateTime::TimeZone::Europe::Kiev 2.47
- DateTime::TimeZone::Europe::Kirov 2.47
- DateTime::TimeZone::Europe::Lisbon 2.47
- DateTime::TimeZone::Europe::London 2.47
- DateTime::TimeZone::Europe::Luxembourg 2.47
- DateTime::TimeZone::Europe::Madrid 2.47
- DateTime::TimeZone::Europe::Malta 2.47
- DateTime::TimeZone::Europe::Minsk 2.47
- DateTime::TimeZone::Europe::Monaco 2.47
- DateTime::TimeZone::Europe::Moscow 2.47
- DateTime::TimeZone::Europe::Oslo 2.47
- DateTime::TimeZone::Europe::Paris 2.47
- DateTime::TimeZone::Europe::Prague 2.47
- DateTime::TimeZone::Europe::Riga 2.47
- DateTime::TimeZone::Europe::Rome 2.47
- DateTime::TimeZone::Europe::Samara 2.47
- DateTime::TimeZone::Europe::Saratov 2.47
- DateTime::TimeZone::Europe::Simferopol 2.47
- DateTime::TimeZone::Europe::Sofia 2.47
- DateTime::TimeZone::Europe::Stockholm 2.47
- DateTime::TimeZone::Europe::Tallinn 2.47
- DateTime::TimeZone::Europe::Tirane 2.47
- DateTime::TimeZone::Europe::Ulyanovsk 2.47
- DateTime::TimeZone::Europe::Uzhgorod 2.47
- DateTime::TimeZone::Europe::Vienna 2.47
- DateTime::TimeZone::Europe::Vilnius 2.47
- DateTime::TimeZone::Europe::Volgograd 2.47
- DateTime::TimeZone::Europe::Warsaw 2.47
- DateTime::TimeZone::Europe::Zaporozhye 2.47
- DateTime::TimeZone::Europe::Zurich 2.47
- DateTime::TimeZone::Floating 2.47
- DateTime::TimeZone::HST 2.47
- DateTime::TimeZone::Indian::Chagos 2.47
- DateTime::TimeZone::Indian::Christmas 2.47
- DateTime::TimeZone::Indian::Cocos 2.47
- DateTime::TimeZone::Indian::Kerguelen 2.47
- DateTime::TimeZone::Indian::Mahe 2.47
- DateTime::TimeZone::Indian::Maldives 2.47
- DateTime::TimeZone::Indian::Mauritius 2.47
- DateTime::TimeZone::Indian::Reunion 2.47
- DateTime::TimeZone::Local 2.47
- DateTime::TimeZone::Local::Android 2.47
- DateTime::TimeZone::Local::Unix 2.47
- DateTime::TimeZone::Local::VMS 2.47
- DateTime::TimeZone::MET 2.47
- DateTime::TimeZone::MST 2.47
- DateTime::TimeZone::MST7MDT 2.47
- DateTime::TimeZone::OffsetOnly 2.47
- DateTime::TimeZone::OlsonDB 2.47
- DateTime::TimeZone::OlsonDB::Change 2.47
- DateTime::TimeZone::OlsonDB::Observance 2.47
- DateTime::TimeZone::OlsonDB::Rule 2.47
- DateTime::TimeZone::OlsonDB::Zone 2.47
- DateTime::TimeZone::PST8PDT 2.47
- DateTime::TimeZone::Pacific::Apia 2.47
- DateTime::TimeZone::Pacific::Auckland 2.47
- DateTime::TimeZone::Pacific::Bougainville 2.47
- DateTime::TimeZone::Pacific::Chatham 2.47
- DateTime::TimeZone::Pacific::Chuuk 2.47
- DateTime::TimeZone::Pacific::Easter 2.47
- DateTime::TimeZone::Pacific::Efate 2.47
- DateTime::TimeZone::Pacific::Enderbury 2.47
- DateTime::TimeZone::Pacific::Fakaofo 2.47
- DateTime::TimeZone::Pacific::Fiji 2.47
- DateTime::TimeZone::Pacific::Funafuti 2.47
- DateTime::TimeZone::Pacific::Galapagos 2.47
- DateTime::TimeZone::Pacific::Gambier 2.47
- DateTime::TimeZone::Pacific::Guadalcanal 2.47
- DateTime::TimeZone::Pacific::Guam 2.47
- DateTime::TimeZone::Pacific::Honolulu 2.47
- DateTime::TimeZone::Pacific::Kiritimati 2.47
- DateTime::TimeZone::Pacific::Kosrae 2.47
- DateTime::TimeZone::Pacific::Kwajalein 2.47
- DateTime::TimeZone::Pacific::Majuro 2.47
- DateTime::TimeZone::Pacific::Marquesas 2.47
- DateTime::TimeZone::Pacific::Nauru 2.47
- DateTime::TimeZone::Pacific::Niue 2.47
- DateTime::TimeZone::Pacific::Norfolk 2.47
- DateTime::TimeZone::Pacific::Noumea 2.47
- DateTime::TimeZone::Pacific::Pago_Pago 2.47
- DateTime::TimeZone::Pacific::Palau 2.47
- DateTime::TimeZone::Pacific::Pitcairn 2.47
- DateTime::TimeZone::Pacific::Pohnpei 2.47
- DateTime::TimeZone::Pacific::Port_Moresby 2.47
- DateTime::TimeZone::Pacific::Rarotonga 2.47
- DateTime::TimeZone::Pacific::Tahiti 2.47
- DateTime::TimeZone::Pacific::Tarawa 2.47
- DateTime::TimeZone::Pacific::Tongatapu 2.47
- DateTime::TimeZone::Pacific::Wake 2.47
- DateTime::TimeZone::Pacific::Wallis 2.47
- DateTime::TimeZone::UTC 2.47
- DateTime::TimeZone::WET 2.47
+ 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
@@ -947,11 +946,11 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
- Devel-StackTrace-2.04
- pathname: D/DR/DROLSKY/Devel-StackTrace-2.04.tar.gz
+ Devel-StackTrace-2.05
+ pathname: D/DR/DROLSKY/Devel-StackTrace-2.05.tar.gz
provides:
- Devel::StackTrace 2.04
- Devel::StackTrace::Frame 2.04
+ Devel::StackTrace 2.05
+ Devel::StackTrace::Frame 2.05
requirements:
ExtUtils::MakeMaker 0
File::Spec 0
@@ -972,76 +971,81 @@ 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.300036
- pathname: R/RJ/RJBS/Email-Sender-1.300036.tar.gz
- provides:
- Email::Sender 1.300036
- Email::Sender::Failure 1.300036
- Email::Sender::Failure::Multi 1.300036
- Email::Sender::Failure::Permanent 1.300036
- Email::Sender::Failure::Temporary 1.300036
- Email::Sender::Manual 1.300036
- Email::Sender::Manual::QuickStart 1.300036
- Email::Sender::Role::CommonSending 1.300036
- Email::Sender::Role::HasMessage 1.300036
- Email::Sender::Simple 1.300036
- Email::Sender::Success 1.300036
- Email::Sender::Success::Partial 1.300036
- Email::Sender::Transport 1.300036
- Email::Sender::Transport::DevNull 1.300036
- Email::Sender::Transport::Failable 1.300036
- Email::Sender::Transport::Maildir 1.300036
- Email::Sender::Transport::Mbox 1.300036
- Email::Sender::Transport::Print 1.300036
- Email::Sender::Transport::SMTP 1.300036
- Email::Sender::Transport::SMTP::Persistent 1.300036
- Email::Sender::Transport::Sendmail 1.300036
- Email::Sender::Transport::Test 1.300036
- Email::Sender::Transport::Wrapper 1.300036
- Email::Sender::Util 1.300036
+ 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 6.78
Fcntl 0
@@ -1063,58 +1067,23 @@ 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-3.10
- pathname: D/DA/DANKOGAI/Encode-3.10.tar.gz
- provides:
- Encode 3.10
- Encode::Alias 2.24
- Encode::Byte 2.04
- Encode::CJKConstants 2.02
- Encode::CN 2.03
- Encode::CN::HZ 2.10
- Encode::Config 2.05
- Encode::EBCDIC 2.02
- Encode::Encoder 2.03
- Encode::Encoding 2.08
- Encode::GSM0338 2.09
- Encode::Guess 2.08
- Encode::JP 2.05
- Encode::JP::H2Z 2.02
- Encode::JP::JIS7 2.08
- Encode::KR 2.03
- Encode::KR::2022_KR 2.04
- Encode::MIME::Header 2.28
- Encode::MIME::Header::ISO_2022_JP 1.09
- Encode::MIME::Name 1.03
- Encode::Symbol 2.02
- Encode::TW 2.03
- Encode::UTF_EBCDIC 3.10
- Encode::Unicode 2.18
- Encode::Unicode::UTF7 2.10
- Encode::XS 3.10
- Encode::utf8 3.10
- encoding 3.00
- requirements:
- Exporter 5.57
- ExtUtils::MakeMaker 0
- Storable 0
- parent 0.221
Encode-Locale-1.05
pathname: G/GA/GAAS/Encode-Locale-1.05.tar.gz
provides:
@@ -1153,11 +1122,11 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- Exporter-Tiny-1.002002
- pathname: T/TO/TOBYINK/Exporter-Tiny-1.002002.tar.gz
+ Exporter-Tiny-1.006002
+ pathname: T/TO/TOBYINK/Exporter-Tiny-1.006002.tar.gz
provides:
- Exporter::Shiny 1.002002
- Exporter::Tiny 1.002002
+ Exporter::Shiny 1.006002
+ Exporter::Tiny 1.006002
requirements:
ExtUtils::MakeMaker 6.17
perl 5.006001
@@ -1210,76 +1179,28 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- ExtUtils-MakeMaker-7.62
- pathname: B/BI/BINGOS/ExtUtils-MakeMaker-7.62.tar.gz
- provides:
- ExtUtils::Command 7.62
- ExtUtils::Command::MM 7.62
- ExtUtils::Liblist 7.62
- ExtUtils::Liblist::Kid 7.62
- ExtUtils::MM 7.62
- ExtUtils::MM_AIX 7.62
- ExtUtils::MM_Any 7.62
- ExtUtils::MM_BeOS 7.62
- ExtUtils::MM_Cygwin 7.62
- ExtUtils::MM_DOS 7.62
- ExtUtils::MM_Darwin 7.62
- ExtUtils::MM_MacOS 7.62
- ExtUtils::MM_NW5 7.62
- ExtUtils::MM_OS2 7.62
- ExtUtils::MM_OS390 7.62
- ExtUtils::MM_QNX 7.62
- ExtUtils::MM_UWIN 7.62
- ExtUtils::MM_Unix 7.62
- ExtUtils::MM_VMS 7.62
- ExtUtils::MM_VOS 7.62
- ExtUtils::MM_Win32 7.62
- ExtUtils::MM_Win95 7.62
- ExtUtils::MY 7.62
- ExtUtils::MakeMaker 7.62
- ExtUtils::MakeMaker::Config 7.62
- ExtUtils::MakeMaker::Locale 7.62
- ExtUtils::MakeMaker::_version 7.62
- ExtUtils::MakeMaker::charstar 7.62
- ExtUtils::MakeMaker::version 7.62
- ExtUtils::MakeMaker::version::regex 7.62
- ExtUtils::MakeMaker::version::vpp 7.62
- ExtUtils::Mkbootstrap 7.62
- ExtUtils::Mksymlists 7.62
- ExtUtils::testlib 7.62
- MM 7.62
- MY 7.62
- requirements:
- Data::Dumper 0
- Encode 0
- File::Basename 0
- File::Spec 0.8
- Pod::Man 0
- perl 5.006
- FFI-CheckLib-0.28
- pathname: P/PL/PLICEASE/FFI-CheckLib-0.28.tar.gz
+ FFI-CheckLib-0.31
+ pathname: P/PL/PLICEASE/FFI-CheckLib-0.31.tar.gz
provides:
- FFI::CheckLib 0.28
+ FFI::CheckLib 0.31
requirements:
ExtUtils::MakeMaker 0
+ File::Which 0
List::Util 1.33
perl 5.006
- File-Listing-6.14
- pathname: P/PL/PLICEASE/File-Listing-6.14.tar.gz
+ File-Listing-6.16
+ pathname: P/PL/PLICEASE/File-Listing-6.16.tar.gz
provides:
- File::Listing 6.14
- File::Listing::apache 6.14
- File::Listing::dosftp 6.14
- File::Listing::netware 6.14
- File::Listing::unix 6.14
- File::Listing::vms 6.14
+ 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:
- Carp 0
- Exporter 0
+ Exporter 5.57
ExtUtils::MakeMaker 0
HTTP::Date 0
- Time::Local 0
- base 0
perl 5.006
File-NFSLock-1.29
pathname: B/BB/BBB/File-NFSLock-1.29.tar.gz
@@ -1299,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
@@ -1338,12 +1259,12 @@ DISTRIBUTIONS
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
@@ -1353,20 +1274,20 @@ DISTRIBUTIONS
perl 5.006
strict 0
vars 0
- GIS-Distance-0.19
- pathname: B/BL/BLUEFEET/GIS-Distance-0.19.tar.gz
- provides:
- GIS::Distance 0.19
- GIS::Distance::ALT 0.19
- GIS::Distance::Constants 0.19
- GIS::Distance::Cosine 0.19
- GIS::Distance::Formula 0.19
- GIS::Distance::GreatCircle 0.19
- GIS::Distance::Haversine 0.19
- GIS::Distance::MathTrig 0.19
- GIS::Distance::Null 0.19
- GIS::Distance::Polar 0.19
- GIS::Distance::Vincenty 0.19
+ 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
@@ -1378,36 +1299,34 @@ DISTRIBUTIONS
parent 0
perl 5.008001
strictures 2.000000
- Geo-Distance-0.25
- pathname: B/BL/BLUEFEET/Geo-Distance-0.25.tar.gz
- provides:
- Geo::Distance 0.25
- 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
- provides:
- Geo::Distance::XS 0.13
- requirements:
- ExtUtils::MakeMaker 0
- Geo::Distance 0.16
- Test::More 0.82
- XSLoader 0
- HTML-Parser-3.76
- pathname: O/OA/OALDERS/HTML-Parser-3.76.tar.gz
+ strictures 2.000000
+ HTML-Parser-3.82
+ pathname: O/OA/OALDERS/HTML-Parser-3.82.tar.gz
provides:
- HTML::Entities 3.76
- HTML::Filter 3.76
- HTML::HeadParser 3.76
- HTML::LinkExtor 3.76
- HTML::Parser 3.76
- HTML::PullParser 3.76
- HTML::TokeParser 3.76
+ 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:
Carp 0
Exporter 0
@@ -1419,18 +1338,19 @@ DISTRIBUTIONS
URI::URL 0
XSLoader 0
strict 0
- HTML-Tagset-3.20
- pathname: P/PE/PETDANCE/HTML-Tagset-3.20.tar.gz
+ HTML-Tagset-3.24
+ pathname: P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz
provides:
- HTML::Tagset 3.20
+ HTML::Tagset 3.24
requirements:
- ExtUtils::MakeMaker 0
- HTTP-Cookies-6.10
- pathname: O/OA/OALDERS/HTTP-Cookies-6.10.tar.gz
+ ExtUtils::MakeMaker 6.46
+ perl 5.010001
+ HTTP-Cookies-6.11
+ pathname: O/OA/OALDERS/HTTP-Cookies-6.11.tar.gz
provides:
- HTTP::Cookies 6.10
- HTTP::Cookies::Microsoft 6.10
- HTTP::Cookies::Netscape 6.10
+ HTTP::Cookies 6.11
+ HTTP::Cookies::Microsoft 6.11
+ HTTP::Cookies::Netscape 6.11
requirements:
Carp 0
ExtUtils::MakeMaker 0
@@ -1440,10 +1360,10 @@ DISTRIBUTIONS
locale 0
perl 5.008001
strict 0
- HTTP-Date-6.05
- pathname: O/OA/OALDERS/HTTP-Date-6.05.tar.gz
+ HTTP-Date-6.06
+ pathname: O/OA/OALDERS/HTTP-Date-6.06.tar.gz
provides:
- HTTP::Date 6.05
+ HTTP::Date 6.06
requirements:
Exporter 0
ExtUtils::MakeMaker 0
@@ -1451,22 +1371,24 @@ DISTRIBUTIONS
Time::Zone 0
perl 5.006002
strict 0
- HTTP-Message-6.32
- pathname: O/OA/OALDERS/HTTP-Message-6.32.tar.gz
- provides:
- HTTP::Config 6.32
- HTTP::Headers 6.32
- HTTP::Headers::Auth 6.32
- HTTP::Headers::ETag 6.32
- HTTP::Headers::Util 6.32
- HTTP::Message 6.32
- HTTP::Request 6.32
- HTTP::Request::Common 6.32
- HTTP::Response 6.32
- HTTP::Status 6.32
+ 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
+ Clone 0.46
+ Compress::Raw::Bzip2 0
+ Compress::Raw::Zlib 2.062
Encode 3.01
Encode::Locale 1
Exporter 5.57
@@ -1477,15 +1399,13 @@ DISTRIBUTIONS
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
URI 1.10
- base 0
+ parent 0
perl 5.008001
strict 0
warnings 0
@@ -1533,18 +1453,19 @@ DISTRIBUTIONS
Exporter 5.57
ExtUtils::MakeMaker 0
perl 5.008
- IO-Socket-SSL-2.071
- pathname: S/SU/SULLR/IO-Socket-SSL-2.071.tar.gz
+ IO-Socket-SSL-2.085
+ pathname: S/SU/SULLR/IO-Socket-SSL-2.085.tar.gz
provides:
- IO::Socket::SSL 2.071
+ IO::Socket::SSL 2.085
IO::Socket::SSL::Intercept 2.056
- IO::Socket::SSL::OCSP_Cache 2.071
- IO::Socket::SSL::OCSP_Resolver 2.071
+ 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.071
- IO::Socket::SSL::SSL_HANDLE 2.071
- IO::Socket::SSL::Session_Cache 2.071
- IO::Socket::SSL::Utils 2.014
+ 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
@@ -1556,11 +1477,11 @@ DISTRIBUTIONS
IO::String 1.08
requirements:
ExtUtils::MakeMaker 0
- JSON-4.03
- pathname: I/IS/ISHIGAKI/JSON-4.03.tar.gz
+ JSON-4.10
+ pathname: I/IS/ISHIGAKI/JSON-4.10.tar.gz
provides:
- JSON 4.03
- JSON::Backend::PP 4.03
+ JSON 4.10
+ JSON::Backend::PP 4.10
requirements:
ExtUtils::MakeMaker 0
Test::More 0
@@ -1585,17 +1506,16 @@ DISTRIBUTIONS
Scalar::Util 0
perl 5.006002
strict 0
- LWP-Protocol-https-6.10
- pathname: O/OA/OALDERS/LWP-Protocol-https-6.10.tar.gz
+ LWP-Protocol-https-6.14
+ pathname: O/OA/OALDERS/LWP-Protocol-https-6.14.tar.gz
provides:
- LWP::Protocol::https 6.10
- LWP::Protocol::https::Socket 6.10
+ LWP::Protocol::https 6.14
+ LWP::Protocol::https::Socket 6.14
requirements:
ExtUtils::MakeMaker 0
- IO::Socket::SSL 1.54
+ IO::Socket::SSL 1.970
LWP::Protocol::http 0
LWP::UserAgent 6.06
- Mozilla::CA 20180117
Net::HTTPS 6
base 0
perl 5.008001
@@ -1635,46 +1555,46 @@ 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
- MIME-tools-5.509
- pathname: D/DS/DSKOLL/MIME-tools-5.509.tar.gz
- provides:
- MIME::Body 5.509
- MIME::Body::File 5.509
- MIME::Body::InCore 5.509
- MIME::Body::Scalar 5.509
- MIME::Decoder 5.509
- MIME::Decoder::Base64 5.509
- MIME::Decoder::BinHex 5.509
- MIME::Decoder::Binary 5.509
- MIME::Decoder::Gzip64 5.509
- MIME::Decoder::NBit 5.509
- MIME::Decoder::QuotedPrint 5.509
- MIME::Decoder::UU 5.509
- MIME::Entity 5.509
- MIME::Field::ConTraEnc 5.509
- MIME::Field::ContDisp 5.509
- MIME::Field::ContType 5.509
- MIME::Field::ParamVal 5.509
- MIME::Head 5.509
- MIME::Parser 5.509
+ 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.509
+ 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.509
+ MIME::Words 5.514
requirements:
ExtUtils::MakeMaker 6.59
File::Path 1
@@ -1689,10 +1609,10 @@ DISTRIBUTIONS
Test::Deep 0
Test::More 0
perl 5.008
- MRO-Compat-0.13
- pathname: H/HA/HAARG/MRO-Compat-0.13.tar.gz
+ 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
@@ -1729,28 +1649,28 @@ DISTRIBUTIONS
Net::Domain 1.05
Net::SMTP 1.03
Test::More 0
- Module-Build-0.4231
- pathname: L/LE/LEONT/Module-Build-0.4231.tar.gz
- provides:
- Module::Build 0.4231
- Module::Build::Base 0.4231
- Module::Build::Compat 0.4231
- Module::Build::Config 0.4231
- Module::Build::Cookbook 0.4231
- Module::Build::Dumper 0.4231
- Module::Build::Notes 0.4231
- Module::Build::PPMMaker 0.4231
- Module::Build::Platform::Default 0.4231
- Module::Build::Platform::MacOS 0.4231
- Module::Build::Platform::Unix 0.4231
- Module::Build::Platform::VMS 0.4231
- Module::Build::Platform::VOS 0.4231
- Module::Build::Platform::Windows 0.4231
- Module::Build::Platform::aix 0.4231
- Module::Build::Platform::cygwin 0.4231
- Module::Build::Platform::darwin 0.4231
- Module::Build::Platform::os2 0.4231
- Module::Build::PodParser 0.4231
+ 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
@@ -1769,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
@@ -1837,10 +1756,10 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- Mojo-Pg-4.25
- pathname: S/SR/SRI/Mojo-Pg-4.25.tar.gz
+ Mojo-Pg-4.27
+ pathname: S/SR/SRI/Mojo-Pg-4.27.tar.gz
provides:
- Mojo::Pg 4.25
+ Mojo::Pg 4.27
Mojo::Pg::Database undef
Mojo::Pg::Migrations undef
Mojo::Pg::PubSub undef
@@ -1852,8 +1771,8 @@ DISTRIBUTIONS
Mojolicious 8.50
SQL::Abstract::Pg 1.0
perl 5.016
- Mojolicious-9.19
- pathname: S/SR/SRI/Mojolicious-9.19.tar.gz
+ Mojolicious-9.36
+ pathname: S/SR/SRI/Mojolicious-9.36.tar.gz
provides:
Mojo undef
Mojo::Asset undef
@@ -1921,7 +1840,7 @@ DISTRIBUTIONS
Mojo::UserAgent::Transactor undef
Mojo::Util undef
Mojo::WebSocket undef
- Mojolicious 9.19
+ Mojolicious 9.36
Mojolicious::Command undef
Mojolicious::Command::Author::cpanify undef
Mojolicious::Command::Author::generate undef
@@ -1970,29 +1889,38 @@ DISTRIBUTIONS
IO::Socket::IP 0.37
Sub::Util 1.41
perl 5.016
- Mojolicious-Plugin-Authentication-1.37
- pathname: J/JJ/JJATRIA/Mojolicious-Plugin-Authentication-1.37.tar.gz
+ Mojolicious-Plugin-Authentication-1.39
+ pathname: J/JJ/JJATRIA/Mojolicious-Plugin-Authentication-1.39.tar.gz
provides:
- Mojolicious::Plugin::Authentication 1.37
+ Mojolicious::Plugin::Authentication 1.39
requirements:
Exporter 0
ExtUtils::MakeMaker 0
Mojolicious 8.0
perl 5.016
- Moo-2.005004
- pathname: H/HA/HAARG/Moo-2.005004.tar.gz
+ 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.005004
+ Moo 2.005005
Moo::HandleMoose undef
Moo::HandleMoose::FakeConstructor undef
Moo::HandleMoose::FakeMetaClass undef
Moo::HandleMoose::_TypeMap undef
Moo::Object undef
- Moo::Role 2.005004
+ Moo::Role 2.005005
Moo::_Utils undef
Moo::sification undef
oo undef
@@ -2014,21 +1942,19 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Module::Runtime 0.014
- Mozilla-CA-20200520
- pathname: A/AB/ABH/Mozilla-CA-20200520.tar.gz
+ Mozilla-CA-20240313
+ pathname: L/LW/LWP/Mozilla-CA-20240313.tar.gz
provides:
- Mozilla::CA 20200520
+ Mozilla::CA 20240313
requirements:
ExtUtils::MakeMaker 0
- Test 0
- perl 5.006
- Net-HTTP-6.21
- pathname: O/OA/OALDERS/Net-HTTP-6.21.tar.gz
+ Net-HTTP-6.23
+ pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz
provides:
- Net::HTTP 6.21
- Net::HTTP::Methods 6.21
- Net::HTTP::NB 6.21
- Net::HTTPS 6.21
+ 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
@@ -2040,20 +1966,24 @@ DISTRIBUTIONS
perl 5.006002
strict 0
warnings 0
- Net-SSLeay-1.90
- pathname: C/CH/CHRISN/Net-SSLeay-1.90.tar.gz
+ Net-SSLeay-1.94
+ pathname: C/CH/CHRISN/Net-SSLeay-1.94.tar.gz
provides:
- Net::SSLeay 1.90
- Net::SSLeay::Handle 1.90
+ 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.39
- pathname: E/ET/ETHER/Package-Stash-0.39.tar.gz
+ Package-Stash-0.40
+ pathname: E/ET/ETHER/Package-Stash-0.40.tar.gz
provides:
- Package::Stash 0.39
- Package::Stash::PP 0.39
+ Package::Stash 0.40
+ Package::Stash::PP 0.40
requirements:
B 0
Carp 0
@@ -2069,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
@@ -2108,12 +2038,12 @@ DISTRIBUTIONS
Scalar::Util 1.18
XSLoader 0.22
parent 0
- Params-ValidationCompiler-0.30
- pathname: D/DR/DROLSKY/Params-ValidationCompiler-0.30.tar.gz
+ 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
@@ -2126,11 +2056,11 @@ DISTRIBUTIONS
overload 0
strict 0
warnings 0
- Path-Tiny-0.118
- pathname: D/DA/DAGOLDEN/Path-Tiny-0.118.tar.gz
+ Path-Tiny-0.144
+ pathname: D/DA/DAGOLDEN/Path-Tiny-0.144.tar.gz
provides:
- Path::Tiny 0.118
- Path::Tiny::Error 0.118
+ Path::Tiny 0.144
+ Path::Tiny::Error 0.144
requirements:
Carp 0
Cwd 0
@@ -2140,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
@@ -2203,49 +2134,49 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
SQL::Abstract 2.0
perl 5.016
- Specio-0.47
- pathname: D/DR/DROLSKY/Specio-0.47.tar.gz
- provides:
- Specio 0.47
- Specio::Coercion 0.47
- Specio::Constraint::AnyCan 0.47
- Specio::Constraint::AnyDoes 0.47
- Specio::Constraint::AnyIsa 0.47
- Specio::Constraint::Enum 0.47
- Specio::Constraint::Intersection 0.47
- Specio::Constraint::ObjectCan 0.47
- Specio::Constraint::ObjectDoes 0.47
- Specio::Constraint::ObjectIsa 0.47
- Specio::Constraint::Parameterizable 0.47
- Specio::Constraint::Parameterized 0.47
- Specio::Constraint::Role::CanType 0.47
- Specio::Constraint::Role::DoesType 0.47
- Specio::Constraint::Role::Interface 0.47
- Specio::Constraint::Role::IsaType 0.47
- Specio::Constraint::Simple 0.47
- Specio::Constraint::Structurable 0.47
- Specio::Constraint::Structured 0.47
- Specio::Constraint::Union 0.47
- Specio::Declare 0.47
- Specio::DeclaredAt 0.47
- Specio::Exception 0.47
- Specio::Exporter 0.47
- Specio::Helpers 0.47
- Specio::Library::Builtins 0.47
- Specio::Library::Numeric 0.47
- Specio::Library::Perl 0.47
- Specio::Library::String 0.47
- Specio::Library::Structured 0.47
- Specio::Library::Structured::Dict 0.47
- Specio::Library::Structured::Map 0.47
- Specio::Library::Structured::Tuple 0.47
- Specio::OO 0.47
- Specio::PartialDump 0.47
- Specio::Registry 0.47
- Specio::Role::Inlinable 0.47
- Specio::Subs 0.47
- Specio::TypeChecks 0.47
- Test::Specio 0.47
+ 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
@@ -2273,18 +2204,18 @@ DISTRIBUTIONS
strict 0
version 0.83
warnings 0
- Sub-Exporter-0.988
- pathname: R/RJ/RJBS/Sub-Exporter-0.988.tar.gz
+ Sub-Exporter-0.991
+ pathname: R/RJ/RJBS/Sub-Exporter-0.991.tar.gz
provides:
- Sub::Exporter 0.988
- Sub::Exporter::Util 0.988
+ Sub::Exporter 0.991
+ Sub::Exporter::Util 0.991
requirements:
Carp 0
Data::OptList 0.100
ExtUtils::MakeMaker 6.78
Params::Util 0.14
Sub::Install 0.92
- perl 5.008000
+ perl 5.012
strict 0
warnings 0
Sub-Exporter-Progressive-0.001013
@@ -2300,105 +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.006006
- pathname: H/HA/HAARG/Sub-Quote-2.006006.tar.gz
+ Sub-Quote-2.006008
+ pathname: H/HA/HAARG/Sub-Quote-2.006008.tar.gz
provides:
- Sub::Defer 2.006006
- Sub::Quote 2.006006
+ Sub::Defer 2.006008
+ Sub::Quote 2.006008
requirements:
ExtUtils::MakeMaker 0
Scalar::Util 0
perl 5.006
- Test-Compile-v2.4.2
- pathname: E/EG/EGILES/Test-Compile-v2.4.2.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 v2.4.2
- Test::Compile::Internal v2.4.2
+ Test::Compile v3.3.1
+ Test::Compile::Internal v3.3.1
requirements:
Exporter 5.68
Module::Build 0.38
- UNIVERSAL::require 0
parent 0.225
perl v5.10.0
- version 0
- Test-Deep-1.130
- pathname: R/RJ/RJBS/Test-Deep-1.130.tar.gz
- provides:
- Test::Deep 1.130
- Test::Deep::All undef
- Test::Deep::Any undef
- Test::Deep::Array undef
- Test::Deep::ArrayEach undef
- Test::Deep::ArrayElementsOnly undef
- Test::Deep::ArrayLength undef
- Test::Deep::ArrayLengthOnly undef
- Test::Deep::Blessed undef
- Test::Deep::Boolean undef
- Test::Deep::Cache undef
- Test::Deep::Cache::Simple undef
- Test::Deep::Class undef
- Test::Deep::Cmp undef
- Test::Deep::Code undef
- Test::Deep::Hash undef
- Test::Deep::HashEach undef
- Test::Deep::HashElements undef
- Test::Deep::HashKeys undef
- Test::Deep::HashKeysOnly undef
- Test::Deep::Ignore undef
- Test::Deep::Isa undef
- Test::Deep::ListMethods undef
- Test::Deep::MM undef
- Test::Deep::Methods undef
- Test::Deep::NoTest undef
- Test::Deep::None undef
- Test::Deep::Number undef
- Test::Deep::Obj undef
- Test::Deep::Ref undef
- Test::Deep::RefType undef
- Test::Deep::Regexp undef
- Test::Deep::RegexpMatches undef
- Test::Deep::RegexpOnly undef
- Test::Deep::RegexpRef undef
- Test::Deep::RegexpRefOnly undef
- Test::Deep::RegexpVersion undef
- Test::Deep::ScalarRef undef
- Test::Deep::ScalarRefOnly undef
- Test::Deep::Set undef
- Test::Deep::Shallow undef
- Test::Deep::Stack undef
- Test::Deep::String undef
- Test::Deep::SubHash undef
- Test::Deep::SubHashElements undef
- Test::Deep::SubHashKeys undef
- Test::Deep::SubHashKeysOnly undef
- Test::Deep::SuperHash undef
- Test::Deep::SuperHashElements undef
- Test::Deep::SuperHashKeys undef
- Test::Deep::SuperHashKeysOnly undef
+ 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 0
+ ExtUtils::MakeMaker 6.78
List::Util 1.09
Scalar::Util 1.09
Test::Builder 0
- Test-Fatal-0.016
- pathname: R/RJ/RJBS/Test-Fatal-0.016.tar.gz
+ Test::More 0.96
+ perl 5.012
+ Test-Differences-0.71
+ pathname: D/DC/DCANTRELL/Test-Differences-0.71.tar.gz
+ provides:
+ 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::Fatal 0.016
+ 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
@@ -2427,18 +2397,30 @@ DISTRIBUTIONS
Test::Builder::Tester 1.02
Test::More 0.62
perl 5.008
- Text-CSV-2.01
- pathname: I/IS/ISHIGAKI/Text-CSV-2.01.tar.gz
+ Text-CSV-2.04
+ pathname: I/IS/ISHIGAKI/Text-CSV-2.04.tar.gz
provides:
- Text::CSV 2.01
- Text::CSV::ErrorDiag 2.01
- Text::CSV_PP 2.01
+ 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:
@@ -2446,6 +2428,20 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Test 0
+ Text-Markdown-1.000031
+ pathname: B/BO/BOBTFISH/Text-Markdown-1.000031.tar.gz
+ provides:
+ 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:
@@ -2475,12 +2471,12 @@ DISTRIBUTIONS
requirements:
Compress::Zlib 0
ExtUtils::MakeMaker 0
- Throwable-0.201
- pathname: R/RJ/RJBS/Throwable-0.201.tar.gz
+ Throwable-1.001
+ pathname: R/RJ/RJBS/Throwable-1.001.tar.gz
provides:
- StackTrace::Auto 0.201
- Throwable 0.201
- Throwable::Error 0.201
+ StackTrace::Auto 1.001
+ Throwable 1.001
+ Throwable::Error 1.001
requirements:
Carp 0
Devel::StackTrace 1.32
@@ -2491,17 +2487,6 @@ DISTRIBUTIONS
Scalar::Util 0
Sub::Quote 0
overload 0
- Time-Local-1.30
- pathname: D/DR/DROLSKY/Time-Local-1.30.tar.gz
- provides:
- Time::Local 1.30
- requirements:
- Carp 0
- Exporter 0
- ExtUtils::MakeMaker 0
- constant 0
- parent 0
- strict 0
TimeDate-2.33
pathname: A/AT/ATOOMIC/TimeDate-2.33.tar.gz
provides:
@@ -2547,12 +2532,12 @@ DISTRIBUTIONS
TimeDate 1.21
requirements:
ExtUtils::MakeMaker 0
- Travel-Status-DE-DBWagenreihung-0.06
- pathname: D/DE/DERF/Travel-Status-DE-DBWagenreihung-0.06.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.06
- Travel::Status::DE::DBWagenreihung::Section 0.06
- Travel::Status::DE::DBWagenreihung::Wagon 0.06
+ 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
@@ -2566,12 +2551,41 @@ DISTRIBUTIONS
Test::Pod 0
Travel::Status::DE::IRIS 1.2
perl v5.20.0
- Travel-Status-DE-IRIS-1.56
- pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.56.tar.gz
+ 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.56
- Travel::Status::DE::IRIS::Result 1.56
- Travel::Status::DE::IRIS::Stations 1.56
+ 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
@@ -2579,7 +2593,7 @@ DISTRIBUTIONS
DateTime::Format::Strptime 0
Encode 0
File::Slurp 9999.19
- Geo::Distance != 0.21
+ GIS::Distance 0
Getopt::Long 0
JSON 0
LWP::Protocol::https 0
@@ -2598,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
@@ -2620,64 +2634,56 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
common::sense 0
- UNIVERSAL-require-0.19
- pathname: N/NE/NEILB/UNIVERSAL-require-0.19.tar.gz
- provides:
- UNIVERSAL::require 0.19
- requirements:
- Carp 0
- ExtUtils::MakeMaker 0
- Test::More 0.47
- perl 5.006
- strict 0
- warnings 0
- URI-5.09
- pathname: O/OA/OALDERS/URI-5.09.tar.gz
- provides:
- URI 5.09
- URI::Escape 5.09
- URI::Heuristic 5.09
- URI::IRI 5.09
- URI::QueryParam 5.09
- URI::Split 5.09
- URI::URL 5.09
- URI::WithBase 5.09
- URI::data 5.09
- URI::file 5.09
- URI::file::Base 5.09
- URI::file::FAT 5.09
- URI::file::Mac 5.09
- URI::file::OS2 5.09
- URI::file::QNX 5.09
- URI::file::Unix 5.09
- URI::file::Win32 5.09
- URI::ftp 5.09
- URI::gopher 5.09
- URI::http 5.09
- URI::https 5.09
- URI::ldap 5.09
- URI::ldapi 5.09
- URI::ldaps 5.09
- URI::mailto 5.09
- URI::mms 5.09
- URI::news 5.09
- URI::nntp 5.09
- URI::nntps 5.09
- URI::pop 5.09
- URI::rlogin 5.09
- URI::rsync 5.09
- URI::rtsp 5.09
- URI::rtspu 5.09
- URI::sftp 5.09
- URI::sip 5.09
- URI::sips 5.09
- URI::snews 5.09
- URI::ssh 5.09
- URI::telnet 5.09
- URI::tn3270 5.09
- URI::urn 5.09
- URI::urn::isbn 5.09
- URI::urn::oid 5.09
+ 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
@@ -2709,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
@@ -2740,45 +2746,45 @@ DISTRIBUTIONS
Fcntl 0
URI 1.10
perl 5.008001
- XML-LibXML-2.0207
- pathname: S/SH/SHLOMIF/XML-LibXML-2.0207.tar.gz
- provides:
- XML::LibXML 2.0207
- XML::LibXML::Attr 2.0207
- XML::LibXML::AttributeHash 2.0207
- XML::LibXML::Boolean 2.0207
- XML::LibXML::CDATASection 2.0207
- XML::LibXML::Comment 2.0207
- XML::LibXML::Common 2.0207
- XML::LibXML::Devel 2.0207
- XML::LibXML::Document 2.0207
- XML::LibXML::DocumentFragment 2.0207
- XML::LibXML::Dtd 2.0207
- XML::LibXML::Element 2.0207
- XML::LibXML::ErrNo 2.0207
- XML::LibXML::Error 2.0207
- XML::LibXML::InputCallback 2.0207
- XML::LibXML::Literal 2.0207
- XML::LibXML::NamedNodeMap 2.0207
- XML::LibXML::Namespace 2.0207
- XML::LibXML::Node 2.0207
- XML::LibXML::NodeList 2.0207
- XML::LibXML::Number 2.0207
- XML::LibXML::PI 2.0207
- XML::LibXML::Pattern 2.0207
- XML::LibXML::Reader 2.0207
- XML::LibXML::RegExp 2.0207
- XML::LibXML::RelaxNG 2.0207
- XML::LibXML::SAX 2.0207
- XML::LibXML::SAX::AttributeNode 2.0207
- XML::LibXML::SAX::Builder 2.0207
- XML::LibXML::SAX::Generator 2.0207
- XML::LibXML::SAX::Parser 2.0207
- XML::LibXML::Schema 2.0207
- XML::LibXML::Text 2.0207
- XML::LibXML::XPathContext 2.0207
- XML::LibXML::XPathExpression 2.0207
- XML::LibXML::_SAXParser 2.0207
+ 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
@@ -2888,56 +2894,55 @@ DISTRIBUTIONS
XSLoader 0
lib 0
perl 5.008001
- libwww-perl-6.55
- pathname: O/OA/OALDERS/libwww-perl-6.55.tar.gz
- provides:
- LWP 6.55
- LWP::Authen::Basic 6.55
- LWP::Authen::Digest 6.55
- LWP::Authen::Ntlm 6.55
- LWP::ConnCache 6.55
- LWP::Debug 6.55
- LWP::Debug::TraceHTTP 6.55
- LWP::DebugFile 6.55
- LWP::MemberMixin 6.55
- LWP::Protocol 6.55
- LWP::Protocol::cpan 6.55
- LWP::Protocol::data 6.55
- LWP::Protocol::file 6.55
- LWP::Protocol::ftp 6.55
- LWP::Protocol::gopher 6.55
- LWP::Protocol::http 6.55
- LWP::Protocol::loopback 6.55
- LWP::Protocol::mailto 6.55
- LWP::Protocol::nntp 6.55
- LWP::Protocol::nogo 6.55
- LWP::RobotUA 6.55
- LWP::Simple 6.55
- LWP::UserAgent 6.55
- 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::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
@@ -2945,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
diff --git a/docker-run.sh b/docker-run.sh
index c6746c3..14e1405 100755
--- a/docker-run.sh
+++ b/docker-run.sh
@@ -1,62 +1,22 @@
-#!/bin/bash
+#!/bin/sh
#
# Copyright (C) Markus Witt
+# Copyright (C) Birte Kristina Friesel
#
# SPDX-License-Identifier: CC0-1.0
-set -eu
-WAIT_DB_HOST=${TRAVELYNX_DB_HOST}
-WAIT_DB_PORT=5432
+set -e
-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
+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
+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/travelynx.conf b/examples/travelynx.conf
index 7f15d12..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 => {
@@ -36,6 +49,14 @@
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"
@@ -76,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 10c6f15..4067b24 100644
--- a/index.pl
+++ b/index.pl
@@ -1,5 +1,5 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 91de1c6..4d04e9e 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -1,6 +1,6 @@
package Travelynx;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -13,14 +13,13 @@ use Cache::File;
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
use DateTime;
use DateTime::Format::Strptime;
-use Encode qw(decode encode);
+use Encode qw(decode encode);
use File::Slurp qw(read_file);
use JSON;
use List::Util;
-use List::UtilsBy qw(uniq_by);
+use List::UtilsBy qw(uniq_by);
use List::MoreUtils qw(first_index);
use Travel::Status::DE::DBWagenreihung;
-use Travel::Status::DE::IRIS::Stations;
use Travelynx::Helper::DBDB;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
@@ -29,14 +28,14 @@ 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;
-use XML::LibXML;
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;
@@ -56,27 +55,6 @@ sub epoch_to_dt {
);
}
-sub get_station {
- my ( $station_name, $exact_match ) = @_;
-
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
-
- if ( @candidates == 1 ) {
- if ( not $exact_match ) {
- return $candidates[0];
- }
- if ( $candidates[0][0] eq $station_name
- or $candidates[0][1] eq $station_name
- or $candidates[0][2] eq $station_name )
- {
- return $candidates[0];
- }
- return undef;
- }
- return undef;
-}
-
sub startup {
my ($self) = @_;
@@ -94,6 +72,7 @@ sub startup {
}
chomp $self->config->{version};
+ $self->defaults( version => $self->config->{version} // 'UNKNOWN' );
$self->plugin(
authentication => {
@@ -121,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.
@@ -128,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.
#
@@ -144,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 );
@@ -182,70 +178,18 @@ 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)];
- }
- );
-
- $self->attr(
- account_public_mask => sub {
- return {
- status_intern => 0x01,
- status_extern => 0x02,
- status_comment => 0x04,
- history_intern => 0x10,
- history_latest => 0x20,
- history_full => 0x40,
- };
- }
- );
-
- $self->attr(
- journey_edit_mask => sub {
- return {
- sched_departure => 0x0001,
- real_departure => 0x0002,
- from_station => 0x0004,
- route => 0x0010,
- is_cancelled => 0x0020,
- sched_arrival => 0x0100,
- real_arrival => 0x0200,
- to_station => 0x0400,
- };
- }
- );
-
- $self->attr(
coordinates_by_station => sub {
my $legacy_names = $self->app->renamed_station;
- my %location;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
- {
- if ( $station->[3] ) {
- $location{ $station->[1] }
- = [ $station->[4], $station->[3] ];
- }
- }
+ my $location = $self->stations->get_latlon_by_name;
while ( my ( $old_name, $new_name ) = each %{$legacy_names} ) {
- $location{$old_name} = $location{$new_name};
+ $location->{$old_name} = $location->{$new_name};
}
- return \%location;
+ 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/BahnhofsAbfahrten/blob/master/src/server/Reihung/ICENaming.ts
+ # 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(
ice_name => sub {
my $id_to_name = JSON->new->utf8->decode(
@@ -262,15 +206,22 @@ sub startup {
}
);
- $self->attr(
- station_by_eva => sub {
- my %map;
- 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} )
{
- $map{ $station->[2] } = $station;
+ return $url;
}
- return \%map;
+ return $self->url_for($path)
+ ->base( $self->app->config->{base_url} );
}
);
@@ -281,7 +232,7 @@ sub startup {
log => $self->app->log,
main_cache => $self->app->cache_iris_main,
realtime_cache => $self->app->cache_iris_rt,
- root_url => $self->url_for('/')->to_abs,
+ root_url => $self->base_url_for('/')->to_abs,
user_agent => $self->ua,
version => $self->app->config->{version},
);
@@ -295,7 +246,7 @@ sub startup {
log => $self->app->log,
main_cache => $self->app->cache_iris_main,
realtime_cache => $self->app->cache_iris_rt,
- root_url => $self->url_for('/')->to_abs,
+ root_url => $self->base_url_for('/')->to_abs,
version => $self->app->config->{version},
);
}
@@ -314,7 +265,7 @@ sub startup {
state $trwl_api = Travelynx::Helper::Traewelling->new(
log => $self->app->log,
model => $self->traewelling,
- root_url => $self->url_for('/')->to_abs,
+ root_url => $self->base_url_for('/')->to_abs,
user_agent => $self->ua,
version => $self->app->config->{version},
);
@@ -346,11 +297,13 @@ sub startup {
journeys => sub {
my ($self) = @_;
state $journeys = Travelynx::Model::Journeys->new(
- log => $self->app->log,
- pg => $self->pg,
- stats_cache => $self->journey_stats_cache,
- renamed_station => $self->app->renamed_station,
- station_by_eva => $self->app->station_by_eva,
+ 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,
);
}
);
@@ -391,6 +344,14 @@ sub startup {
);
$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 );
@@ -403,7 +364,7 @@ sub startup {
state $dbdb = Travelynx::Helper::DBDB->new(
log => $self->app->log,
cache => $self->app->cache_iris_main,
- root_url => $self->url_for('/')->to_abs,
+ root_url => $self->base_url_for('/')->to_abs,
user_agent => $self->ua,
version => $self->app->config->{version},
);
@@ -433,69 +394,104 @@ sub startup {
);
$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 );
- }
+ 'sprintf_km' => sub {
+ my ( $self, $km ) = @_;
+
+ if ( $km < 1 ) {
+ return sprintf( '%.f m', $km * 1000 );
+ }
+ if ( $km < 10 ) {
+ return sprintf( '%.1f km', $km );
}
- return @unknown_stations;
+ return sprintf( '%.f km', $km );
+ }
+ );
+
+ $self->helper(
+ '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(
- 'checkin' => sub {
+ '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';
+ }
+ if ( $visibility eq 'private' ) {
+ return 'lock';
+ }
+ return 'help_outline';
+ }
+ );
+
+ $self->helper(
+ 'checkin_p' => sub {
my ( $self, %opt ) = @_;
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 $db = $opt{db} // $self->pg->db;
+ my $hafas;
+
+ 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 ( $train_id =~ m{[|]} ) {
+ return $self->_checkin_hafas_p(%opt);
+ }
+
+ my $promise = Mojo::Promise->new;
- my $status = $self->iris->get_departures(
+ $self->iris->get_departures_p(
station => $station,
lookbehind => 140,
lookahead => 40
- );
- 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 {
+ )->then(
+ sub {
+ my ($status) = @_;
- my $user = $self->get_user_status( $uid, $db );
- if ( $user->{checked_in} or $user->{cancelled} ) {
+ if ( $status->{errstr} ) {
+ $promise->reject( $status->{errstr} );
+ return;
+ }
- if ( $user->{train_id} eq $train_id
- and $user->{dep_eva} eq $status->{station_eva} )
- {
- # checking in twice is harmless
- return ( $train, undef );
- }
+ my $eva = $status->{station_eva};
+ my $train = List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- # Otherwise, someone forgot to check out first
- $self->checkout(
- station => $station,
- force => 1,
- uid => $uid,
- db => $db
- );
+ if ( not defined $train ) {
+ $promise->reject("Train ${train_id} not found");
+ return;
}
eval {
$self->in_transit->add(
uid => $uid,
db => $db,
- departure_eva => $status->{station_eva},
+ departure_eva => $eva,
train => $train,
route => [ $self->iris->route_diff($train) ],
);
@@ -503,17 +499,151 @@ sub startup {
if ($@) {
$self->app->log->error(
"Checkin($uid): INSERT failed: $@");
- return ( undef, 'INSERT failed: ' . $@ );
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
}
- if ( not $opt{in_transaction} ) {
- # mustn't be called during a transaction
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
$self->add_route_timestamps( $uid, $train, 1 );
$self->run_hook( $uid, 'checkin' );
}
- return ( $train, undef );
+
+ $promise->resolve($train);
+ return;
}
- }
+ )->catch(
+ sub {
+ my ( $err, $status ) = @_;
+ $promise->reject( $status->{errstr} );
+ return;
+ }
+ )->wait;
+
+ return $promise;
+ }
+ );
+
+ $self->helper(
+ '_checkin_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
+
+ 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 $promise = Mojo::Promise->new;
+
+ $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 )
+ {
+ $found = $stop;
+ last;
+ }
+ }
+ 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 {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): 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,
+ };
+ }
+ }
+
+ 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;
}
);
@@ -554,6 +684,26 @@ sub startup {
delete $journey->{edited};
delete $journey->{id};
+ # 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
@@ -587,27 +737,41 @@ sub startup {
);
$self->helper(
- 'checkout' => sub {
+ 'checkout_p' => sub {
my ( $self, %opt ) = @_;
- my $station = $opt{station};
- my $force = $opt{force};
- my $uid = $opt{uid};
- my $db = $opt{db} // $self->pg->db;
- my $status = $self->iris->get_departures(
- station => $station,
- lookbehind => 120,
- lookahead => 120
- );
- $uid //= $self->current_user->{id};
- my $user = $self->get_user_status( $uid, $db );
- my $train_id = $user->{train_id};
+ 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};
+
+ my $promise = Mojo::Promise->new;
+
+ 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' );
+ return $promise->resolve( 0,
+ 'You are not checked into any train' );
}
- if ( $status->{errstr} and not $force ) {
- return ( 1, $status->{errstr} );
+
+ if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
+ }
+ if ( $arr_eva and $arr_eva != $user->{arr_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
+ }
+
+ if ( $train_id =~ m{[|]} ) {
+ return $self->_checkout_hafas_p(%opt);
}
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@@ -616,151 +780,306 @@ sub startup {
with_data => 1
);
- # 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} };
-
- $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->iris->get_departures(
- station => $station,
- lookbehind => 120,
- lookahead => 180,
- with_related => 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;
- }
- }
+ $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
+ );
- # Store the intended checkout station regardless of this operation's
- # success.
- $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 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
- );
- }
+ # 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 ( not defined $train ) {
+ $train //= List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- # 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+$} )
+ if ( not defined $train ) {
+
+ # 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} }
+ )
{
- $rt_arr->add( minutes => $station_data->{adelay} );
+ $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
+ );
+ }
}
- $self->in_transit->set_arrival_times(
- uid => $uid,
- db => $db,
- sched_arrival => $sched_arr,
- rt_arrival => $rt_arr
+ if ( not $force ) {
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ }
+ $promise->resolve( 1, undef );
+ return;
+ }
+ }
+ my $has_arrived = 0;
+
+ eval {
+
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
+
+ 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) ]
+ );
+
+ $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 )
+ )
+ );
+ }
+ }
+ }
+
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
);
+
+ if ( $has_arrived or $force ) {
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->delete(
+ uid => $uid,
+ db => $db
+ );
+
+ 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
+ );
+ }
+ elsif ( defined $train
+ and $train->arrival_is_cancelled )
+ {
+
+ # 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,
+ );
+ }
+
+ if ( not $opt{in_transaction} ) {
+ $tx->commit;
+ }
+ };
+
+ if ($@) {
+ $self->app->log->error("Checkout($uid): $@");
+ $promise->resolve( 1, 'Checkout error: ' . $@ );
+ return;
}
- }
- if ( not $force ) {
- # mustn't be called during a transaction
+ 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 );
}
- return ( 1, undef );
+ $promise->resolve( 1, undef );
+ return;
+
}
- }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->resolve( 1, $err );
+ return;
+ }
+ )->wait;
- my $has_arrived = 0;
+ return $promise;
+ }
+ );
- eval {
+ $self->helper(
+ '_checkout_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
- my $tx;
- if ( not $opt{in_transaction} ) {
- $tx = $db->begin;
- }
+ my $station = $opt{station};
+ my $force = $opt{force};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
- 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) ]
- );
+ my $promise = Mojo::Promise->new;
- $has_arrived = $train->arrival->epoch < $now->epoch ? 1 : 0;
- if ($has_arrived) {
- my @unknown_stations
- = $self->grep_unknown_stations( $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 $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,
+ );
+
+ # 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;
}
+ }
+ if ( not $found ) {
+ return $promise->resolve( 1, 'station not found in route' );
+ }
- $journey = $self->in_transit->get(
- uid => $uid,
- db => $db
- );
+ eval {
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
if ( $has_arrived or $force ) {
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
$self->journeys->add_from_in_transit(
db => $db,
journey => $journey
@@ -785,48 +1104,27 @@ sub startup {
uid => $uid
);
}
- elsif ( defined $train and $train->arrival_is_cancelled ) {
-
- # 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,
- );
- }
- if ( not $opt{in_transaction} ) {
+ if ($tx) {
$tx->commit;
}
};
if ($@) {
$self->app->log->error("Checkout($uid): $@");
- return ( 1, 'Checkout error: ' . $@ );
+ return $promise->resolve( 1, 'Checkout error: ' . $@ );
}
if ( $has_arrived or $force ) {
if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'checkout' );
}
- return ( 0, undef );
+ return $promise->resolve( 0, undef );
}
if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'update' );
- $self->add_route_timestamps( $uid, $train, 0 );
}
- return ( 1, undef );
+ return $promise->resolve( 1, undef );
}
);
@@ -839,92 +1137,7 @@ sub startup {
$uid //= $self->current_user->{id};
- return $self->users->get_data( uid => $uid );
- }
- );
-
- $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 );
}
);
@@ -932,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 )
{
@@ -942,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,
@@ -967,12 +1180,20 @@ 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();
@@ -982,7 +1203,12 @@ sub startup {
)->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();
}
@@ -992,165 +1218,34 @@ sub startup {
}
);
+ # 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;
-# 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 $journey = $self->in_transit->get(
+ # 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;
}
- if ( $journey->{data}{trip_id}
- and not $journey->{polyline} )
- {
- my ( $origin_eva, $destination_eva, $polyline_str );
- $self->hafas->get_polyline_p( $train,
- $journey->{data}{trip_id} )->then(
- sub {
- my ($ret) = @_;
- my $polyline = $ret->{polyline};
- $origin_eva = 0 + $ret->{raw}{origin}{id};
- $destination_eva = 0 + $ret->{raw}{destination}{id};
-
- # work around Cache::File turning floats into strings
- for my $coord ( @{$polyline} ) {
- @{$coord} = map { 0 + $_ } @{$coord};
- }
-
- $polyline_str = JSON->new->encode($polyline);
-
- my $pl_res = $db->select(
- 'polylines',
- ['id'],
- {
- origin_eva => $origin_eva,
- destination_eva => $destination_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 => $origin_eva,
- destination_eva => $destination_eva,
- polyline => $polyline_str
- },
- { returning => 'id' }
- )->hash->{id};
- };
- if ($@) {
- $self->app->log->warn(
- "add_route_timestamps: insert polyline: $@"
- );
- }
- }
- if ($polyline_id) {
- $self->in_transit->set_polyline_id(
- uid => $uid,
- db => $db,
- polyline_id => $polyline_id
- );
- }
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- if ( $err =~ m{extra content at the end}i ) {
- $self->app->log->debug(
- "add_route_timestamps: $err");
- }
- else {
- $self->app->log->warn("add_route_timestamps: $err");
- }
- return;
- }
- )->wait;
- }
-
- my ($platform) = ( ( $train->platform // 0 ) =~ m{(\d+)} );
-
- my $route = $journey->{route};
+ my $route = $in_transit->{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->hafas->get_json_p(
- "${base}&date=${date_yy}&trainname=${train_no}")->then(
+ $self->hafas->get_tripid_p( train => $train )->then(
sub {
- my ($trainsearch) = @_;
-
- # Fallback: Take first result
- my $result = $trainsearch->{suggestions}[0];
- $trainlink = $result->{trainLink};
-
- # 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}
- and ( $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 ) {
- $result = $suggestion;
- $trainlink = $suggestion->{trainLink};
- last;
- }
- }
- }
-
- if ( not $trainlink ) {
- $self->app->log->debug("trainlink not found");
- return Mojo::Promise->reject("trainlink not found");
- }
-
- # Calculate and store trip_id.
- # The trip_id's date part doesn't seem to matter -- so far,
- # HAFAS is happy as long as the date part starts with a number.
- # HAFAS-internal tripIDs use this format (withouth leading zero
- # for day of month < 10) though, so let's stick with it.
- my $date_map = $date_yyyy;
- $date_map =~ tr{.}{}d;
- my $trip_id = sprintf( '1|%d|%d|%d|%s',
- $result->{id}, $result->{cycle},
- $result->{pool}, $date_map );
+ my ($trip_id) = @_;
$self->in_transit->update_data(
uid => $uid,
@@ -1158,64 +1253,66 @@ sub startup {
data => { trip_id => $trip_id }
);
- my $base2
- = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';
- return $self->hafas->get_json_p(
-"${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_json.vs_hap"
+ 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 ($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 ( $route_data, $journey, $polyline ) = @_;
- my $strp = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%y %H:%M',
- time_zone => 'Europe/Berlin',
- );
-
- $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->hafas->get_xml_p(
- "${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3"
- );
- }
- )->then(
- sub {
- my ($traininfo2) = @_;
-
- 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;
+ }
+
+ # 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};
+ }
}
}
- for my $station ( @{$route} ) {
- $station->[1]
- = $route_data->{ $station->[0] };
+ my @messages;
+ for my $m ( $journey->messages ) {
+ if ( not $m->code ) {
+ push(
+ @messages,
+ {
+ header => $m->short,
+ lead => $m->text,
+ }
+ );
+ }
}
$self->in_transit->set_route_data(
@@ -1230,21 +1327,24 @@ sub startup {
map { [ $_->[0]->epoch, $_->[1] ] }
$train->qos_messages
],
- him_messages => $traininfo2->{messages},
+ him_messages => \@messages,
);
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ old_id => $in_transit->{polyline_id},
+ );
+ }
+
return;
}
)->catch(
sub {
my ($err) = @_;
- if ( $err
- =~ m{trainlink not found|extra content at the end}i )
- {
- $self->app->log->debug("add_route_timestamps: $err");
- }
- else {
- $self->app->log->warn("add_route_timestamps: $err");
- }
+ $self->app->log->debug("add_route_timestamps: $err");
return;
}
)->wait;
@@ -1282,7 +1382,7 @@ sub startup {
push(
@wagons,
{
- id => $wagon->{fahrzeugnummer},
+ id => $wagon->{fahrzeugnummer},
number =>
$wagon->{wagenordnungsnummer},
type => $wagon->{fahrzeugtyp},
@@ -1301,6 +1401,12 @@ sub startup {
wagons => [@wagons],
}
);
+ if ( $group->{fahrzeuggruppebezeichnung}
+ and $group->{fahrzeuggruppebezeichnung} eq
+ 'ICE0304' )
+ {
+ $data->{wagonorder_pride} = 1;
+ }
}
$self->in_transit->update_data(
uid => $uid,
@@ -1334,7 +1440,7 @@ sub startup {
}
if ($is_departure) {
- $self->dbdb->get_stationinfo_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 };
@@ -1354,8 +1460,8 @@ sub startup {
)->wait;
}
- if ( $journey->{arr_eva} and not $is_departure ) {
- $self->dbdb->get_stationinfo_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 };
@@ -1378,249 +1484,16 @@ sub startup {
);
$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;
-
- if (
- my $id = $self->in_transit->get_checkout_station_id(
- uid => $uid,
- db => $db
- )
- )
- {
- return $id;
- }
-
- return $self->journeys->get_latest_checkout_station_id(
- uid => $uid,
- db => $db
- );
- }
- );
-
- $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;
-
- if ( $opt{destination_name} ) {
- return ( $opt{destination_name} );
- }
-
- 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->users->use_history( uid => $uid );
-
- my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
- my $now = $self->now->epoch;
- my ( $stationinfo, $arr_epoch, $arr_platform );
-
- if ( $opt{eva} ) {
- if ( $use_history & 0x01 ) {
- $eva = $opt{eva};
- }
- 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;
- }
- }
- }
-
- $exclude_before //= $now - 300;
-
- 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->iris->get_departures(
- station => $eva,
- lookbehind => 10,
- lookahead => 40,
- with_related => 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
- 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 ] );
-
- # Show all past and up to two future departures per destination
- if ( not $train->departure
- or $train->departure->epoch >= $now )
- {
- $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;
-
- 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} ) {
- $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} = 'warning';
- }
- elsif ( $interchange_time == $interchange_duration ) {
- $train->{interchange_text}
- = 'Anschluss könnte knapp werden';
- $train->{interchange_icon} = 'directions_run';
- }
-
- #else {
- # $train->{interchange_text} = 'Anschluss wird voraussichtlich erreicht';
- # $train->{interchange_icon} = 'check';
- #}
- }
- }
-
- return ( @results, @cancellations );
+ '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;
}
);
@@ -1688,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 );
}
@@ -1724,157 +1593,75 @@ sub startup {
uid => $uid,
db => $db,
with_data => 1,
- with_timestamps => 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} )
- {
- $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} )
+ $ret->{traewelling} = $traewelling;
+ if ( @{ $traewelling->{data}{log} // [] }
+ and ( my $log_entry = $traewelling->{data}{log}[0] ) )
{
- $is_after = 1;
- if ( @{$station} > 1 and not $dep_info ) {
- $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} )
@@ -1893,85 +1680,32 @@ 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, $latest_cancellation ) = $self->journeys->get_latest(
uid => $uid,
- db => $db
+ db => $db,
);
if ( $latest_cancellation and $latest_cancellation->{cancelled} ) {
- if ( my $station
- = $self->app->station_by_eva
- ->{ $latest_cancellation->{dep_eva} } )
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{dep_eva}
+ )
+ )
{
- $latest_cancellation->{dep_ds100} = $station->[0];
- $latest_cancellation->{dep_name} = $station->[1];
+ $latest_cancellation->{dep_ds100} = $station->{ds100};
+ $latest_cancellation->{dep_name} = $station->{name};
}
- if ( my $station
- = $self->app->station_by_eva
- ->{ $latest_cancellation->{arr_eva} } )
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{arr_eva}
+ )
+ )
{
- $latest_cancellation->{arr_ds100} = $station->[0];
- $latest_cancellation->{arr_name} = $station->[1];
+ $latest_cancellation->{arr_ds100} = $station->{ds100};
+ $latest_cancellation->{arr_name} = $station->{name};
}
}
else {
@@ -1982,16 +1716,16 @@ sub startup {
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,
@@ -2009,14 +1743,23 @@ 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},
};
}
@@ -2033,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,
@@ -2044,12 +1788,13 @@ 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,
+ longitude => $status->{dep_lon},
+ latitude => $status->{dep_lat},
scheduledTime => $status->{sched_departure}
? $status->{sched_departure}->epoch
: undef,
@@ -2061,8 +1806,8 @@ sub startup {
ds100 => $status->{arr_ds100},
name => $status->{arr_name},
uic => $status->{arr_eva},
- longitude => undef,
- latitude => undef,
+ longitude => $status->{arr_lon},
+ latitude => $status->{arr_lat},
scheduledTime => $status->{sched_arrival}
? $status->{sched_arrival}->epoch
: undef,
@@ -2071,17 +1816,31 @@ sub startup {
: 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}
- ? $status->{timestamp}->epoch
- : undef,
intermediateStops => [],
+ visibility => {
+ level => $status->{effective_visibility},
+ desc => $status->{effective_visibility_str},
+ }
};
+ if ( $opt{public} ) {
+ if ( not $privacy->{comments_visible} ) {
+ delete $ret->{comment};
+ }
+ }
+ else {
+ $ret->{actionTime}
+ = $status->{timestamp}
+ ? $status->{timestamp}->epoch
+ : undef;
+ }
+
for my $stop ( @{ $status->{route_after} // [] } ) {
if ( $status->{arr_name} and $stop->[0] eq $status->{arr_name} )
{
@@ -2091,64 +1850,40 @@ sub startup {
@{ $ret->{intermediateStops} },
{
name => $stop->[0],
- scheduledArrival => $stop->[1]{sched_arr}
- ? $stop->[1]{sched_arr}->epoch
+ scheduledArrival => $stop->[2]{sched_arr}
+ ? $stop->[2]{sched_arr}->epoch
: undef,
- realArrival => $stop->[1]{rt_arr}
- ? $stop->[1]{rt_arr}->epoch
+ realArrival => $stop->[2]{rt_arr}
+ ? $stop->[2]{rt_arr}->epoch
: undef,
- scheduledDeparture => $stop->[1]{sched_dep}
- ? $stop->[1]{sched_dep}->epoch
+ scheduledDeparture => $stop->[2]{sched_dep}
+ ? $stop->[2]{sched_dep}->epoch
: undef,
- realDeparture => $stop->[1]{rt_dep}
- ? $stop->[1]{rt_dep}->epoch
+ realDeparture => $stop->[2]{rt_dep}
+ ? $stop->[2]{rt_dep}->epoch
: undef,
}
);
}
- 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 ( $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] };
- }
- }
-
return $ret;
}
);
$self->helper(
- 'traewelling_to_travelynx' => sub {
+ 'traewelling_to_travelynx_p' => sub {
my ( $self, %opt ) = @_;
my $traewelling = $opt{traewelling};
my $user_data = $opt{user_data};
my $uid = $user_data->{user_id};
+ my $promise = Mojo::Promise->new;
+
if ( not $traewelling->{checkin}
or $self->now->epoch - $traewelling->{checkin}->epoch > 900 )
{
$self->log->debug("... not checked in");
- return;
+ return $promise->resolve;
}
if ( $traewelling->{status_id}
and $user_data->{data}{latest_pull_status_id}
@@ -2156,106 +1891,61 @@ sub startup {
== $user_data->{data}{latest_pull_status_id} )
{
$self->log->debug("... already handled");
- return;
+ return $promise->resolve;
}
- $self->log->debug("... checked in");
+ $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;
+ return $promise->resolve;
}
if ( $traewelling->{category}
!~ m{^ (?: national .* | regional .* | suburban ) $ }x )
{
- $self->log->debug(
- "... status is not a train, but $traewelling->{category}");
- $self->traewelling->log(
- uid => $uid,
- message =>
-"$traewelling->{line} nach $traewelling->{arr_name} ist keine Zugfahrt (HAFAS-Kategorie '$traewelling->{category}')",
- status_id => $traewelling->{status_id},
- );
- $self->traewelling->set_latest_pull_status_id(
- uid => $uid,
- status_id => $traewelling->{status_id}
- );
- return;
- }
-
- my $dep = $self->iris->get_departures(
- station => $traewelling->{dep_eva},
- lookbehind => 60,
- lookahead => 40
- );
- if ( $dep->{errstr} ) {
- $self->traewelling->log(
- uid => $uid,
- message =>
-"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $dep->{errstr}",
- status_id => $traewelling->{status_id},
- is_error => 1,
- );
- return;
- }
- my ( $train_ref, $train_id );
- 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 ($train_id) {
- $self->log->debug("... found train: $train_id");
my $db = $self->pg->db;
my $tx = $db->begin;
- my ( undef, $err ) = $self->checkin(
+ $self->checkin_p(
station => $traewelling->{dep_eva},
- train_id => $train_id,
+ train_id => $traewelling->{trip_id},
uid => $uid,
in_transaction => 1,
db => $db
- );
-
- if ( not $err ) {
- ( undef, $err ) = $self->checkout(
- station => $traewelling->{arr_eva},
- train_id => 0,
- uid => $uid,
- in_transaction => 1,
- db => $db
- );
- if ( not $err ) {
- $self->log->debug("... success!");
+ )->then(
+ sub {
+ $self->log->debug("... handled origin");
+ return $self->checkout_p(
+ station => $traewelling->{arr_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ );
+ }
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
if ( $traewelling->{message} ) {
$self->in_transit->update_user_data(
- uid => $uid,
- db => $db,
+ uid => $uid,
+ db => $db,
user_data =>
{ comment => $traewelling->{message} }
);
}
$self->traewelling->log(
- uid => $uid,
- db => $db,
+ uid => $uid,
+ db => $db,
message =>
"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
status_id => $traewelling->{status_id},
@@ -2267,28 +1957,171 @@ sub startup {
);
$tx->commit;
+ $promise->resolve;
+ return;
}
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ $promise->resolve;
+ return;
+ }
+ )->wait;
+ return $promise;
+ }
+
+ $self->iris->get_departures_p(
+ station => $traewelling->{dep_eva},
+ lookbehind => 60,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($dep) = @_;
+ my ( $train_ref, $train_id );
+
+ if ( $dep->{errstr} ) {
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+ status_id => $traewelling->{status_id},
+ is_error => 1,
+ );
+ $promise->resolve;
+ return;
+ }
+
+ for my $train ( @{ $dep->{results} } ) {
+ if ( $train->line ne $traewelling->{line} ) {
+ next;
+ }
+ if ( not $train->sched_departure
+ or $train->sched_departure->epoch
+ != $traewelling->{dep_dt}->epoch )
+ {
+ next;
+ }
+ if (
+ not
+ List::Util::first { $_ eq $traewelling->{arr_name} }
+ $train->route_post
+ )
+ {
+ next;
+ }
+ $train_id = $train->train_id;
+ $train_ref = $train;
+ last;
+ }
+
+ if ( not $train_id ) {
+ $self->log->debug(
+ "... train $traewelling->{line} not found");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: Zug nicht gefunden",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ return $promise->resolve;
+ }
+
+ $self->log->debug("... found train: $train_id");
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->checkin_p(
+ station => $traewelling->{dep_eva},
+ train_id => $train_id,
+ 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;
}
- if ($err) {
- $self->log->debug("... error: $err");
+ )->catch(
+ sub {
+ my ( $err, $dep ) = @_;
$self->traewelling->log(
- uid => $uid,
+ uid => $uid,
message =>
-"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $err",
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
status_id => $traewelling->{status_id},
- is_error => 1
+ is_error => 1,
);
+ $promise->resolve;
+ return;
}
- }
- else {
- $self->traewelling->log(
- uid => $uid,
- message =>
-"$traewelling->{line} nach $traewelling->{arr_name} nicht gefunden",
- status_id => $traewelling->{status_id},
- is_error => 1
- );
- }
+ )->wait;
+
+ return $promise;
}
);
@@ -2415,16 +2248,16 @@ sub startup {
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.
+ # 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 )
@@ -2470,7 +2303,11 @@ sub startup {
{
polylines => $json->encode( \@station_pairs ),
color => '#673ab7',
- opacity => $with_polyline ? 0.4 : 0.6,
+ opacity => @polylines
+ ? $with_polyline
+ ? 0.4
+ : 0.6
+ : 0.8,
},
{
polylines => $json->encode( \@polylines ),
@@ -2481,8 +2318,8 @@ sub startup {
};
if (@station_coordinates) {
- my @lats = map { $_->[0][0] } @station_coordinates;
- my @lons = map { $_->[0][1] } @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;
@@ -2514,26 +2351,35 @@ 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('/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('/p/:name')->to('traveling#public_profile');
- $r->get('/p/:name/j/:id')->to('traveling#public_journey_details');
+ $r->get('/status/:name')->to('profile#user_status');
+ $r->get('/status/:name/:ts')->to('profile#user_status');
+ $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('/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');
@@ -2545,16 +2391,24 @@ sub startup {
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');
@@ -2568,27 +2422,35 @@ sub startup {
$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('/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 4f7c792..d13b2a7 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1,10 +1,13 @@
package Travelynx::Command::database;
-# Copyright (C) 2020 Daniel Friesel
+
+# 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';
@@ -12,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.
@@ -1055,8 +1057,1085 @@ my @migrations = (
}
);
},
+
+ # 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;
@@ -1070,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 (
@@ -1103,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);
}
}
@@ -1121,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 {
@@ -1149,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 b1a0ae9..600ffb0 100644
--- a/lib/Travelynx/Command/dumpconfig.pm
+++ b/lib/Travelynx/Command/dumpconfig.pm
@@ -1,5 +1,5 @@
package Travelynx::Command::dumpconfig;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
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 5f609cb..c9c7ed6 100644
--- a/lib/Travelynx/Command/maintenance.pm
+++ b/lib/Travelynx/Command/maintenance.pm
@@ -1,6 +1,6 @@
package Travelynx::Command::maintenance;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
@@ -14,10 +14,11 @@ has usage => sub { shift->extract_usage };
sub run {
my ( $self, $filename ) = @_;
- 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 $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;
@@ -82,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 );
@@ -97,34 +126,30 @@ 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 $hooks_res = $db->delete( 'webhooks', { user_id => $uid } );
- my $trwl_res = $db->delete( 'traewelling', { user_id => $uid } );
-
- # TODO + traewelling, webhooks
- 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;
@@ -144,94 +169,6 @@ sub run {
);
$tx->commit;
}
-
- # Add estimated polylines to journeys logged before 2020-01-28
-
- $tx = $db->begin;
-
- say 'Adding polylines to journeys logged before 2020-01-28';
- my $no_polyline
- = $db->select( 'journeys', 'count(*) as count', { polyline_id => undef } )
- ->hash;
- say "Checking $no_polyline->{count} journeys ...";
-
- for my $journey (
- $db->select( 'journeys', [ 'id', 'route' ], { polyline_id => undef } )
- ->hashes->each )
- {
-
- # prior to v1.9.4, routes were stored as [["stop1"], ["stop2"], ...].
- # Nowadays, the common format is [["stop1", {}, null], ...].
- # entry[1] is non-empty only while checked in, entry[2] is non-null only
- # if the stop is unscheduled or has been cancelled.
- #
- # Here, we pretend to use the new format, as we're looking for
- # matching routes in more recent journeys.
- #
- # Note that journey->{route} is serialized JSON (i.e., a string).
- # It is not deserialized for performance reasons.
- $journey->{route}
- =~ s/ (?<! additional ) (?<! cancelled ) "] /", {}, null]/gx;
-
- my $ref = $db->select(
- 'journeys',
- [ 'id', 'polyline_id' ],
- {
- route => $journey->{route},
- polyline_id => { '!=', undef },
- edited => 0,
- },
- { limit => 1 }
- )->hash;
- if ($ref) {
- my $rows = $db->update(
- 'journeys',
- { polyline_id => $ref->{polyline_id} },
- { id => $journey->{id} }
- )->rows;
- if ( $rows != 1 ) {
- say STDERR
-"Database update returned $rows rows, expected 1. Rollback and abort.";
- exit(1);
- }
- }
- else {
- while ( my ( $old_name, $new_name )
- = each %{ $self->app->renamed_station } )
- {
- $journey->{route} =~ s{"\Q$old_name\E"}{"$new_name"};
- }
- my $ref = $db->select(
- 'journeys',
- [ 'id', 'polyline_id' ],
- {
- route => $journey->{route},
- polyline_id => { '!=', undef },
- edited => 0,
- },
- { limit => 1 }
- )->hash;
- if ($ref) {
- my $rows = $db->update(
- 'journeys',
- { polyline_id => $ref->{polyline_id} },
- { id => $journey->{id} }
- )->rows;
- if ( $rows != 1 ) {
- say STDERR
-"Database update returned $rows rows, expected 1. Rollback and abort.";
- exit(1);
- }
- }
- }
- }
-
- my $remaining
- = $db->select( 'journeys', 'count(*) as count', { polyline_id => undef } )
- ->hash;
- say "Done! Remaining journeys without polyline: " . $remaining->{count};
-
- $tx->commit;
}
1;
diff --git a/lib/Travelynx/Command/munin.pm b/lib/Travelynx/Command/munin.pm
index 9c53ece..3b6e393 100644
--- a/lib/Travelynx/Command/munin.pm
+++ b/lib/Travelynx/Command/munin.pm
@@ -1,6 +1,6 @@
package Travelynx::Command::munin;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
@@ -70,6 +70,12 @@ sub run {
->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(
'polyline_ratio',
$db->query(
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 24621b5..10b1b69 100644
--- a/lib/Travelynx/Command/work.pm
+++ b/lib/Travelynx/Command/work.pm
@@ -1,16 +1,16 @@
package Travelynx::Command::work;
-# Copyright (C) 2020 Daniel Friesel
+# 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 };
@@ -21,24 +21,94 @@ sub run {
my $checkin_deadline = $now->clone->subtract( hours => 48 );
my $json = JSON->new;
- my $db = $self->app->pg->db;
+ my $num_incomplete = $self->app->in_transit->delete_incomplete_checkins(
+ earlier_than => $checkin_deadline );
- my $res = $db->delete( 'in_transit',
- { checkin_time => { '<', $checkin_deadline } } );
-
- if ( my $rows = $res->rows ) {
- $self->app->log->debug("Removed ${rows} incomplete checkins");
+ 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
@@ -60,72 +130,43 @@ 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;
}
- # selecting on user_id and train_no avoids a race condition when
- # 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(
- [ $self->app->iris->route_diff($train) ]
- ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- ),
- },
- {
- user_id => $uid,
- train_no => $train->train_no
- }
+ $self->app->in_transit->update_departure(
+ uid => $uid,
+ train => $train,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ route => [ $self->app->iris->route_diff($train) ]
);
+
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. Ensure that the user is still checked into this train
- # before calling checkout to mark the cancellation.
- if (
- $db->select(
- 'in_transit',
- 'count(*) as count',
- {
- user_id => $uid,
- train_no => $train->train_no,
- checkin_station_id => $dep,
- checkout_station_id => $arr,
- }
- )->hash->{count}
- )
- {
- $db->update(
- 'in_transit',
- {
- cancelled => 1,
- },
- {
- user_id => $uid,
- train_no => $train->train_no,
- checkin_station_id => $dep,
- checkout_station_id => $arr,
- }
- );
+ # 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(
+ # check out (adds a cancelled journey and resets journey state
+ # to checkin
+ $self->app->checkout_p(
station => $arr,
- force => 1,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
uid => $uid
- );
+ )->wait;
}
}
else {
@@ -134,6 +175,7 @@ sub run {
}
};
if ($@) {
+ $errors += 1;
$self->app->log->error("work($uid)/departure: $@");
}
@@ -173,128 +215,80 @@ sub run {
return;
}
- # 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.
- $db->update(
- 'in_transit',
- {
- arr_platform => $train->platform,
- sched_arrival => $train->sched_arrival,
- real_arrival => $train->arrival,
- route => $json->encode(
- [ $self->app->iris->route_diff($train) ]
- ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- ),
- },
- {
- user_id => $uid,
- train_no => $train->train_no,
- checkout_station_id => $arr
- }
+ my $checked_in = $self->app->in_transit->update_arrival(
+ uid => $uid,
+ train => $train,
+ route => [ $self->app->iris->route_diff($train) ],
+ dep_eva => $dep,
+ arr_eva => $arr,
);
- if ( $train->arrival_is_cancelled ) {
- # 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
- # before calling checkout to mark the cancellation.
- if (
- $db->select(
- 'in_transit',
- 'count(*) as count',
- {
- user_id => $uid,
- train_no => $train->train_no,
- checkout_station_id => $arr
- }
- )->hash->{count}
- )
- {
- # check out (adds a cancelled journey and resets journey state
- # to destination selection)
- $self->app->checkout(
- station => $arr,
- force => 0,
- uid => $uid
- );
- }
+ 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 );
+ $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(
+ my ( undef, $error ) = $self->app->checkout_p(
station => $arr,
- force => 1,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
uid => $uid
- );
- if ($error) {
- die("${error}\n");
- }
+ )->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 { };
}
- for my $account_data ( $self->app->traewelling->get_pull_accounts ) {
-
- # $account_data->{user_id} is the travelynx uid
- # $account_data->{user_name} is the Träwelling username
- $self->app->log->debug(
- "Pulling Traewelling status for UID $account_data->{user_id}");
- $self->app->traewelling_api->get_status_p(
- username => $account_data->{data}{user_name},
- token => $account_data->{token}
- )->then(
- sub {
- my ($traewelling) = @_;
- $self->app->traewelling_to_travelynx(
- traewelling => $traewelling,
- user_data => $account_data
- );
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->app->log->debug("Error $err");
- }
- )->wait;
- }
+ my $started_at = $now;
+ my $main_finished_at = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $worker_duration = $main_finished_at->epoch - $started_at->epoch;
- 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 =>
-"Fehler bei $candidate->{train_type} $candidate->{train_no}: Keine trip_id vorhanden",
- is_error => 1
+ 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}"
);
- next;
}
- if ( $candidate->{data}{latest_push_ts}
- and $candidate->{data}{latest_push_ts} == $candidate->{checkin_ts} )
- {
- $self->app->log->debug("... already handled");
- next;
+ else {
+ $self->app->ua->post_p( $self->app->config->{influxdb}->{url},
+ "worker runtime_seconds=${worker_duration},errors=${errors}" )
+ ->wait;
}
- $self->app->traewelling_api->checkin( %{$candidate},
- trip_id => $trip_id );
+ }
+
+ 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 e40c034..be7431f 100644
--- a/lib/Travelynx/Command/worker.pm
+++ b/lib/Travelynx/Command/worker.pm
@@ -1,27 +1,31 @@
package Travelynx::Command::worker;
-# Copyright (C) 2020 Daniel Friesel
+
+# 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;
}
}
@@ -36,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 12a059a..f1dc43e 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1,25 +1,228 @@
package Travelynx::Controller::Account;
-# Copyright (C) 2020 Daniel Friesel
+# 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');
@@ -35,8 +238,9 @@ sub do_login {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'login',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
}
else {
@@ -47,10 +251,18 @@ sub do_login {
else {
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'
+ );
}
}
}
@@ -69,9 +281,6 @@ sub register {
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
@@ -79,8 +288,9 @@ sub register {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'register',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
@@ -88,17 +298,21 @@ sub register {
if ( my $registration_denylist
= $self->app->config->{registration}->{denylist} )
{
- open( my $fh, "<", $registration_denylist )
- or die("cannot open($registration_denylist)");
- while ( my $line = <$fh> ) {
- chomp $line;
- if ( $ip eq $line ) {
- close($fh);
- $self->render( 'register', invalid => "denylist" );
- return;
+ 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: $!");
}
- close($fh);
}
if ( my $error = $self->users->is_name_invalid( name => $user ) ) {
@@ -132,47 +346,31 @@ sub register {
# a human user should take at least five seconds to fill out the form.
# Throw a CSRF error at presumed spammers.
$self->render(
- 'register',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
my $token = make_token();
- my $pw_hash = hash_password($password);
my $db = $self->pg->db;
my $tx = $db->begin;
- my $user_id = $self->users->add_user(
- db => $db,
- name => $user,
- email => $email,
- token => $token,
- password_hash => $pw_hash
+ my $user_id = $self->users->add(
+ db => $db,
+ name => $user,
+ email => $email,
+ token => $token,
+ password => $password,
);
- 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";
- 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' );
@@ -209,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;
}
@@ -222,13 +425,14 @@ sub delete {
)
)
{
- $self->render( 'account', invalid => 'deletion password' );
+ $self->flash( invalid => 'deletion password' );
+ $self->redirect_to('account');
return;
}
- $self->users->flag_deletion( uid => $self->current_user->{id} );
+ $self->users->flag_deletion( uid => $uid );
}
else {
- $self->users->unflag_deletion( uid => $self->current_user->{id} );
+ $self->users->unflag_deletion( uid => $uid );
}
$self->redirect_to('account');
}
@@ -236,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;
@@ -246,75 +454,346 @@ 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;
}
- if ( $self->param('history_level') eq 'intern' ) {
- $public_level |= 0x10;
- $public_level &= ~0x20;
+ $self->users->set_social(
+ uid => $user->{id},
+ %opt
+ );
+
+ $self->flash( success => 'social' );
+ $self->redirect_to('account');
+ }
+ else {
+ if ( $user->{accept_follows} ) {
+ $self->param( accept_follow => 'yes' );
}
- elsif ( $self->param('history_level') eq 'extern' ) {
- $public_level |= 0x20;
- $public_level &= ~0x10;
+ elsif ( $user->{accept_follow_requests} ) {
+ $self->param( accept_follow => 'request' );
}
else {
- $public_level &= ~0x30;
+ $self->param( accept_follow => 'no' );
}
+ $self->render( 'social', name => $user->{name} );
+ }
+}
- if ( $self->param('history_age') eq 'infinite' ) {
- $public_level |= 0x40;
- }
- else {
- $public_level &= ~0x40;
- }
+sub social_list {
+ my ($self) = @_;
- $self->users->set_privacy(
- uid => $user->{id},
- level => $public_level
- );
+ my $kind = $self->stash('kind');
+ my $user = $self->current_user;
- $self->flash( success => 'privacy' );
- $self->redirect_to('account');
+ 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},
+ );
+ }
+ 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->param(
- status_level => $public_level & 0x01 ? 'intern'
- : $public_level & 0x02 ? 'extern'
- : 'private'
+ $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->param( public_comment => $public_level & 0x04 ? 1 : 0 );
- $self->param(
- history_level => $public_level & 0x10 ? 'intern'
- : $public_level & 0x20 ? 'extern'
- : 'private'
+ $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->param(
- history_age => $public_level & 0x40 ? 'infinite' : 'month' );
- $self->render( 'privacy', name => $user->{name} );
+ $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 {
@@ -352,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}
@@ -372,7 +877,7 @@ sub webhook {
sub {
$self->render(
'webhooks',
- hook => $self->get_webhook,
+ hook => $self->users->get_webhook( uid => $uid ),
new_hook => 1
);
}
@@ -398,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;
}
@@ -421,7 +927,6 @@ sub change_mail {
}
my $token = make_token();
- my $name = $self->current_user->{name};
my $db = $self->pg->db;
my $tx = $db->begin;
@@ -432,34 +937,7 @@ sub change_mail {
token => $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 );
+ my $success = $self->send_address_confirmation_mail( $email, $token );
if ($success) {
$tx->commit;
@@ -485,9 +963,9 @@ sub change_name {
if ( $action and $action eq 'update_name' ) {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
- 'change_name',
- name => $old_name,
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
@@ -510,10 +988,10 @@ sub change_name {
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.
+ # 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
@@ -531,32 +1009,7 @@ sub change_name {
$self->flash( success => 'name' );
$self->redirect_to('account');
- 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 .= "Alter Name: ${old_name}\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";
-
- $self->sendmail->custom( $self->current_user->{email},
- 'travelynx: Name geändert', $body );
+ $self->send_name_notification_mail( $old_name, $new_name );
}
else {
$self->render( 'change_name', name => $old_name );
@@ -576,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;
}
@@ -601,37 +1058,14 @@ sub change_password {
return;
}
- my $pw_hash = hash_password($password);
- $self->users->set_password_hash(
- uid => $self->current_user->{id},
- password_hash => $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 {
@@ -639,7 +1073,11 @@ 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;
}
@@ -672,36 +1110,12 @@ sub request_password_reset {
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;
@@ -720,7 +1134,11 @@ 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 (
@@ -743,10 +1161,9 @@ sub request_password_reset {
return;
}
- my $pw_hash = hash_password($password);
- $self->users->set_password_hash(
- uid => $id,
- password_hash => $pw_hash
+ $self->users->set_password(
+ uid => $id,
+ password => $password
);
my $account = $self->get_user_data($id);
@@ -764,31 +1181,7 @@ sub request_password_reset {
token => $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->send_lostpassword_notification_mail($account);
}
else {
$self->render('recover_password');
@@ -825,6 +1218,15 @@ sub confirm_mail {
my $id = $self->current_user->{id};
my $token = $self->stash('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,
@@ -841,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->users->mark_seen( uid => $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 {
@@ -868,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 974b9ca..687243d 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -1,15 +1,17 @@
package Travelynx::Controller::Api;
-# Copyright (C) 2020 Daniel Friesel
+# 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);
}
@@ -28,10 +30,22 @@ sub sanitize {
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 {
@@ -67,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',
@@ -77,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(
@@ -130,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 => {
@@ -150,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;
@@ -160,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} )
)
)
{
@@ -174,131 +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 (
- @{
- [
- Travel::Status::DE::IRIS::Stations::get_station(
- $from_station)
- ]
- } != 1
- )
- {
+ if ( not $hafas and not $self->stations->search($from_station) ) {
$self->render(
json => {
success => \0,
deprecated => \0,
- error => 'fromStation is ambiguous',
- status => $self->get_user_status_json_v1($uid)
+ error => 'Unknown fromStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
- if (
- $to_station
- and @{
- [
- Travel::Status::DE::IRIS::Stations::get_station(
- $to_station)
- ]
- } != 1
- )
+ if ( $to_station
+ and not $hafas
+ and not $self->stations->search($to_station) )
{
$self->render(
json => {
success => \0,
deprecated => \0,
- error => 'toStation is ambiguous',
- status => $self->get_user_status_json_v1($uid)
+ error => 'Unknown toStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
},
);
return;
}
- if ( exists $payload->{train}{id} ) {
- $train_id = sanitize( 0, $payload->{train}{id} );
+ 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->iris->get_departures(
+
+ $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 );
+ }
);
- if ( $status->{errstr} ) {
+ }
+
+ $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(
- station => $from_station,
- train_id => $train_id,
- uid => $uid
- );
- if ( $payload->{comment} and not $error ) {
- $self->in_transit->update_user_data(
- uid => $uid,
- user_data => { comment => sanitize( q{}, $payload->{comment} ) }
- );
- }
- if ( $to_station and not $error ) {
- ( $train, $error ) = $self->checkout(
- station => $to_station,
- force => 0,
- uid => $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} );
@@ -309,7 +336,7 @@ 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;
@@ -322,30 +349,43 @@ sub travel_v1 {
);
}
- my ( $train, $error ) = $self->checkout(
- station => $to_station,
- force => $payload->{force} ? 1 : 0,
- uid => $uid
- );
- if ($error) {
- $self->render(
- json => {
- success => \0,
- deprecated => \0,
- error => '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)
+ $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 );
@@ -355,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 )
}
);
}
@@ -364,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 )
}
);
}
@@ -413,7 +453,7 @@ sub import_v1 {
return;
}
- my $token = $self->get_api_token($uid);
+ my $token = $self->users->get_api_token( uid => $uid );
if ( not $token->{'import'} or $api_token ne $token->{'import'} ) {
$self->render(
json => {
@@ -457,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(
@@ -568,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');
@@ -605,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 1503483..d80f1ae 100644
--- a/lib/Travelynx/Controller/Passengerrights.pm
+++ b/lib/Travelynx/Controller/Passengerrights.pm
@@ -1,5 +1,6 @@
package Travelynx::Controller::Passengerrights;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
@@ -7,12 +8,15 @@ 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;
@@ -85,6 +89,8 @@ sub mark_substitute_connection {
}
}
+# Controllers
+
sub list_candidates {
my ($self) = @_;
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
new file mode 100755
index 0000000..a063c10
--- /dev/null
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -0,0 +1,576 @@
+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->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} );
+
+ 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->render( 'not_found', status => 404 );
+ }
+ return;
+ }
+ $self->render( '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 addcd61..04c2d0f 100644
--- a/lib/Travelynx/Controller/Static.pm
+++ b/lib/Travelynx/Controller/Static.pm
@@ -1,29 +1,32 @@
package Travelynx::Controller::Static;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
-my $travelynx_version = qx{git describe --dirty} || 'experimental';
-
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
index e906b1f..3cdeff8 100644
--- a/lib/Travelynx/Controller/Traewelling.pm
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -1,59 +1,97 @@
package Travelynx::Controller::Traewelling;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Promise;
-sub settings {
+sub oauth {
my ($self) = @_;
- my $uid = $self->current_user->{id};
-
if ( $self->param('action')
and $self->validation->csrf_protect->has_error('csrf_token') )
{
$self->render(
- 'traewelling',
- invalid => 'csrf',
+ 'bad_request',
+ csrf => 1,
+ status => 400
);
return;
}
- if ( $self->param('action') and $self->param('action') eq 'login' ) {
- my $email = $self->param('email');
- my $password = $self->param('password');
- $self->render_later;
- $self->traewelling_api->login_p(
- uid => $uid,
- email => $email,
- password => $password
- )->then(
- sub {
- my $traewelling = $self->traewelling->get($uid);
- $self->param( sync_source => 'none' );
- $self->render(
- 'traewelling',
- traewelling => $traewelling,
- new_traewelling => 1,
- );
+ $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;
}
- )->catch(
- sub {
- my ($err) = @_;
- $self->render(
- 'traewelling',
- traewelling => {},
- new_traewelling => 1,
- login_error => $err,
- );
+ 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;
}
- )->wait;
+ 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;
}
- elsif ( $self->param('action') and $self->param('action') eq 'logout' ) {
+
+ if ( $self->param('action') and $self->param('action') eq 'logout' ) {
$self->render_later;
- my $traewelling = $self->traewelling->get($uid);
+ my $traewelling = $self->traewelling->get( uid => $uid );
$self->traewelling_api->logout_p(
uid => $uid,
token => $traewelling->{token}
@@ -78,17 +116,17 @@ sub settings {
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,
+ 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,
+ 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);
+ my $traewelling = $self->traewelling->get( uid => $uid );
if ( $traewelling->{push_sync} ) {
$self->param( sync_source => 'travelynx' );
@@ -106,6 +144,7 @@ sub settings {
$self->param( tweet => 1 );
}
+ $self->stash( title => 'travelynx × träwelling' );
$self->render(
'traewelling',
traewelling => $traewelling,
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index ffc4211..89385e1 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,338 +1,465 @@
package Travelynx::Controller::Traveling;
-# Copyright (C) 2020 Daniel Friesel
+# 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 JSON;
-use List::Util qw(uniq min max);
-use List::UtilsBy qw(max_by 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->users->mark_seen( uid => $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) = @_;
-
- my $name = $self->stash('name');
- my $ts = $self->stash('ts') // 0;
- my $user = $self->users->get_privacy_by_name( name => $name );
+sub get_connecting_trains_p {
+ my ( $self, %opt ) = @_;
- if ( not $user or not $user->{public_level} & 0x03 ) {
- $self->render('not_found');
- return;
- }
+ my $uid = $opt{uid} //= $self->current_user->{id};
+ my $use_history = $self->users->use_history( uid => $uid );
- if ( $user->{public_level} & 0x01 and not $self->is_user_authenticated ) {
- $self->render( 'login', redirect_to => $self->req->url );
- return;
- }
+ my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
+ my $now = $self->now->epoch;
+ my ( $stationinfo, $arr_epoch, $arr_platform, $arr_countdown );
- my $status = $self->get_user_status( $user->{id} );
- my $journey;
+ my $promise = Mojo::Promise->new;
- if (
- $ts
- and ( not $status->{checked_in}
- or $status->{sched_departure}->epoch != $ts )
- and ( $user->{public_level} & 0x20
- or
- ( $user->{public_level} & 0x10 and $self->is_user_authenticated ) )
- )
- {
- for my $candidate (
- $self->journeys->get(
- uid => $user->{id},
- limit => 10,
- )
- )
- {
- if ( $candidate->{sched_dep_ts} eq $ts ) {
- $journey = $self->journeys->get_single(
- uid => $user->{id},
- journey_id => $candidate->{id},
- verbose => 1,
- with_datetime => 1,
- with_polyline => 1,
- );
- }
+ if ( $opt{eva} ) {
+ if ( $use_history & 0x01 ) {
+ $eva = $opt{eva};
}
- }
-
- 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 ($journey) {
- $og_data{title} = $tw_data{title} = sprintf( 'Fahrt von %s nach %s',
- $journey->{from_name}, $journey->{to_name} );
- $og_data{description} = $tw_data{description}
- = $journey->{rt_arrival}->strftime('Ankunft am %d.%m.%Y um %H:%M');
- $og_data{url} .= "/${ts}";
- }
- elsif (
- $ts
- and ( not $status->{checked_in}
- or $status->{sched_departure}->epoch != $ts )
- )
- {
- $og_data{title} = $tw_data{title} = "Bahnfahrt beendet";
- $og_data{description} = $tw_data{description}
- = "${name} hat das Ziel erreicht";
- }
- elsif ( $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');
+ elsif ( $opt{destination_name} ) {
+ $eva = $opt{eva};
}
}
else {
- $og_data{title} = $tw_data{title}
- = "${name} ist gerade nicht eingecheckt";
- $og_data{description} = $tw_data{description}
- = "Letztes Fahrtziel: $status->{arr_name}";
- }
-
- if ($journey) {
- if ( not $user->{public_level} & 0x04 ) {
- delete $journey->{user_data}{comment};
+ 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};
+ }
}
- my $map_data = $self->journeys_to_map_data(
- journeys => [$journey],
- include_manual => 1,
- );
- $self->render(
- 'journey',
- error => undef,
- with_map => 1,
- readonly => 1,
- journey => $journey,
- twitter => \%tw_data,
- opengraph => \%og_data,
- %{$map_data},
- );
}
- else {
- $self->render(
- 'user_status',
- name => $name,
- public_level => $user->{public_level},
- journey => $status,
- twitter => \%tw_data,
- opengraph => \%og_data,
- );
+
+ $exclude_before //= $now - 300;
+
+ if ( not $eva ) {
+ return $promise->reject;
}
-}
-sub public_profile {
- my ($self) = @_;
+ my ( $dest_ids, $destinations )
+ = $self->journeys->get_connection_targets(%opt);
- my $name = $self->stash('name');
- my $user = $self->users->get_privacy_by_name( name => $name );
+ my @destinations = uniq_by { $_->{name} } @{$destinations};
- if (
- $user
- and ( $user->{public_level} & 0x22
- or
- ( $user->{public_level} & 0x11 and $self->is_user_authenticated ) )
- )
- {
- my $status = $self->get_user_status( $user->{id} );
- my @journeys;
- if ( $user->{public_level} & 0x40 ) {
- @journeys = $self->journeys->get(
- uid => $user->{id},
- limit => 10,
- with_datetime => 1
- );
- }
- else {
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $month_ago = $now->clone->subtract( weeks => 4 );
- @journeys = $self->journeys->get(
- uid => $user->{id},
- limit => 10,
- with_datetime => 1,
- after => $month_ago,
- before => $now
- );
- }
- $self->render(
- 'profile',
- name => $name,
- uid => $user->{id},
- public_level => $user->{public_level},
- journey => $status,
- journeys => [@journeys],
- version => $self->app->config->{version} // 'UNKNOWN',
- );
+ if ($exclude_via) {
+ @destinations = grep { $_->{name} ne $exclude_via } @destinations;
}
- else {
- $self->render('not_found');
+
+ if ( not @destinations ) {
+ return $promise->reject;
}
-}
-sub public_journey_details {
- my ($self) = @_;
- my $name = $self->stash('name');
- my $journey_id = $self->stash('id');
- my $user = $self->users->get_privacy_by_name( name => $name );
+ my $iris_eva = $eva;
+ if ( $eva < 8000000 ) {
+ $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} )
+ // $eva;
+ }
- $self->param( journey_id => $journey_id );
+ my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0;
+ my $lookahead
+ = $can_check_in ? 40 : ( ( ${arr_countdown} // 0 ) / 60 + 40 );
- if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
- $self->render(
- 'journey',
- status => 404,
- error => 'notfound',
- journey => {}
- );
- return;
- }
+ my $iris_promise = Mojo::Promise->new;
+ my %via_count = map { $_->{name} => 0 } @destinations;
- if (
- $user
- and ( $user->{public_level} & 0x20
- or
- ( $user->{public_level} & 0x10 and $self->is_user_authenticated ) )
- )
+ if ( $iris_eva >= 8000000
+ and List::Util::any { $_->{eva} >= 8000000 } @destinations )
{
- my $journey = $self->journeys->get_single(
- uid => $user->{id},
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
- with_polyline => 1,
- );
+ $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;
+ }
- if ( not( $user->{public_level} & 0x40 ) ) {
- my $month_ago = DateTime->now( time_zone => 'Europe/Berlin' )
- ->subtract( weeks => 4 )->epoch;
- if ( $journey and $journey->{rt_dep_ts} < $month_ago ) {
- $journey = undef;
- }
- }
+ @{ $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;
+ }
+ }
+ }
+ }
- if ($journey) {
- my $title = sprintf( 'Fahrt von %s nach %s am %s',
- $journey->{from_name}, $journey->{to_name},
- $journey->{rt_arrival}->strftime('%d.%m.%Y') );
- my $description = sprintf( 'Ankunft mit %s %s %s',
- $journey->{type}, $journey->{no},
- $journey->{rt_arrival}->strftime('um %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,
- );
+ @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;
+ }
- my $map_data = $self->journeys_to_map_data(
- journeys => [$journey],
- include_manual => 1,
- );
- if ( $journey->{user_data}{comment}
- and not $user->{public_level} & 0x04 )
- {
- delete $journey->{user_data}{comment};
+ # 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;
}
- $self->render(
- 'journey',
- error => undef,
- journey => $journey,
- with_map => 1,
- username => $name,
- readonly => 1,
- twitter => \%tw_data,
- opengraph => \%og_data,
- %{$map_data},
- );
- }
- else {
- $self->render('not_found');
- }
+ )->catch(
+ sub {
+ $iris_promise->resolve( [] );
+ return;
+ }
+ )->wait;
}
else {
- $self->render('not_found');
+ $iris_promise->resolve( [] );
}
-}
-sub public_status_card {
- my ($self) = @_;
+ 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: $@"
+ );
+ }
- my $name = $self->stash('name');
- my $user = $self->users->get_privacy_by_name( name => $name );
+ $promise->resolve( \@iris_trains, \@hafas_trains );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
- delete $self->stash->{layout};
+ return $promise;
+}
- 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} );
+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 );
}
}
@@ -342,14 +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( '_cancelled_departure',
- journey => $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 );
}
}
@@ -362,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],
+ $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 ];
}
- } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
- $lat, 10 );
- @candidates = uniq_by { $_->{name} } @candidates;
- if ( @candidates > 5 ) {
+ 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;
@@ -428,67 +667,124 @@ sub log_action {
if ( $params->{action} eq 'checkin' ) {
- my ( $train, $error ) = $self->checkin(
- station => $params->{station},
- train_id => $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(
- station => $destination,
- force => 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(
+ $self->render_later;
+ my $status = $self->get_user_status;
+ $self->checkout_p(
station => $params->{station},
force => $params->{force}
- );
- my $station_link = '/s/' . $params->{station};
+ )->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;
@@ -504,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 => {
@@ -515,50 +816,69 @@ sub log_action {
}
}
elsif ( $params->{action} eq 'cancelled_from' ) {
- my ( undef, $error ) = $self->checkin(
+ $self->render_later;
+ $self->checkin_p(
station => $params->{station},
train_id => $params->{train}
- );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
- }
+ )->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(
+ $self->render_later;
+ $self->checkout_p(
station => $params->{station},
force => 1
- );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
- }
+ )->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->journeys->delete(
@@ -595,57 +915,271 @@ sub log_action {
}
sub station {
- my ($self) = @_;
- my $station = $self->stash('station');
- my $train = $self->param('train');
-
- my $status = $self->iris->get_departures(
- station => $station,
- lookbehind => 120,
- lookahead => 30,
- with_related => 1
+ 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 );
- if ( $status->{errstr} ) {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- with_autocomplete => 1,
- with_geolocation => 1,
- error => $status->{errstr}
+ $self->render_later;
+
+ 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',
- eva => $status->{station_eva},
- 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,
+ );
+ }
+ else {
+ $promise = $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 120,
+ lookahead => 30,
+ with_related => 1,
);
}
- $self->users->mark_seen( uid => $self->current_user->{id} );
+ $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 {
@@ -668,7 +1202,10 @@ sub cancelled {
sub history {
my ($self) = @_;
- $self->render( template => 'history' );
+ $self->render(
+ template => 'history',
+ title => 'travelynx: History'
+ );
}
sub commute {
@@ -719,10 +1256,10 @@ sub commute {
$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.
+ # 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} }--;
}
@@ -776,6 +1313,7 @@ sub commute {
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)
],
@@ -792,13 +1330,64 @@ sub map_history {
}
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
+ 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',
@@ -820,7 +1409,9 @@ sub map_history {
$self->render(
template => 'history_map',
+ year => $year,
with_map => 1,
+ title => 'travelynx: Karte',
%{$res}
);
}
@@ -886,42 +1477,126 @@ sub csv_history {
);
}
-sub yearly_history {
+sub year_in_review {
my ($self) = @_;
my $year = $self->stash('year');
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->journeys->get(
- uid => $self->current_user->{id},
- 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_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 ( not @journeys ) {
+ $self->render(
+ 'not_found',
+ message => 'Keine Zugfahrten im angefragten Jahr gefunden.',
+ status => 404
);
- 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
+ return;
+ }
+
+ 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
);
- $stats = $self->journeys->get_stats(
- uid => $self->current_user->{id},
- year => $year
+ return;
+ }
+
+ my ( $stats, $review ) = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year,
+ review => 1
+ );
+
+ $self->render(
+ 'year_in_review',
+ title => "travelynx: Jahresrückblick $year",
+ year => $year,
+ stats => $stats,
+ review => $review,
+ );
+
+}
+
+sub yearly_history {
+ my ($self) = @_;
+ my $year = $self->stash('year');
+ my $filter = $self->param('filter');
+ my @journeys;
+
+ # 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;
+ }
+ 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.'
);
+ 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(
@@ -932,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
}
);
@@ -946,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)
@@ -961,35 +1637,43 @@ sub monthly_history {
and $month < 13 )
)
{
- @journeys = $self->journeys->get(
- uid => $self->current_user->{id},
- 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->journeys->get(
- uid => $self->current_user->{id},
- after => $interval_start,
- before => $interval_end,
- with_datetime => 1
- );
- $stats = $self->journeys->get_stats(
- uid => $self->current_user->{id},
- 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 => {
@@ -999,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
}
);
@@ -1013,7 +1698,8 @@ 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 );
@@ -1028,11 +1714,12 @@ sub journey_details {
}
my $journey = $self->journeys->get_single(
- uid => $uid,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
- with_polyline => 1,
+ uid => $uid,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
);
if ($journey) {
@@ -1040,11 +1727,53 @@ sub journey_details {
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,
- with_map => 1,
+ 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},
);
}
@@ -1059,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');
@@ -1099,11 +1916,13 @@ sub comment_form {
}
else {
$self->app->log->debug("set comment");
+ my $uid = $self->current_user->{id};
$self->in_transit->update_user_data(
- uid => $self->current_user->{id},
+ uid => $uid,
user_data => { comment => $self->param('comment') }
);
$self->redirect_to('/');
+ $self->run_hook( $uid, 'update' );
}
}
@@ -1293,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;
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
index 4baf3ed..b98a372 100644
--- a/lib/Travelynx/Helper/DBDB.pm
+++ b/lib/Travelynx/Helper/DBDB.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::DBDB;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -30,11 +30,11 @@ sub has_wagonorder_p {
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}";
+ = "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($url) ) {
+ if ( my $content = $cache->get("HEAD $url") ) {
if ( $content eq 'n' ) {
return $promise->reject;
}
@@ -43,24 +43,23 @@ sub has_wagonorder_p {
}
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
if ( $tx->result->is_success ) {
- my $body = $tx->result->body;
- $cache->set( $url, $body );
- $promise->resolve($body);
+ $cache->set( "HEAD $url", 'a' );
+ $promise->resolve('a');
}
else {
- $cache->set( $url, 'n' );
+ $cache->set( "HEAD $url", 'n' );
$promise->reject;
}
return;
}
)->catch(
sub {
- $cache->set( $url, 'n' );
+ $cache->set( "HEAD $url", 'n' );
$promise->reject;
return;
}
@@ -74,11 +73,6 @@ sub get_wagonorder_p {
my $url
= "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
- if ( $api !~ m{i} and $api =~ m{a} ) {
- $url
- = "https://www.apps-bahn.de/wr/wagenreihung/1.0/${train_no}/${api_ts}";
- }
-
my $cache = $self->{cache};
my $promise = Mojo::Promise->new;
@@ -91,11 +85,17 @@ sub get_wagonorder_p {
->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);
+ 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(
diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm
index 6fd5c71..7671d78 100644
--- a/lib/Travelynx/Helper/HAFAS.pm
+++ b/lib/Travelynx/Helper/HAFAS.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::HAFAS;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,7 +12,13 @@ use DateTime;
use Encode qw(decode);
use JSON;
use Mojo::Promise;
-use XML::LibXML;
+use Travel::Status::DE::HAFAS;
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
sub new {
my ( $class, %opt ) = @_;
@@ -27,15 +33,16 @@ sub new {
return bless( \%opt, $class );
}
-sub get_polyline_p {
- my ( $self, $train, $trip_id ) = @_;
+sub get_json_p {
+ my ( $self, $url, %opt ) = @_;
- my $line = $train->line // 0;
- my $url
- = "https://v5.db.transport.rest/trips/${trip_id}?lineName=${line}&polyline=true";
my $cache = $self->{main_cache};
my $promise = Mojo::Promise->new;
- my $version = $self->{version};
+
+ if ( $opt{realtime} ) {
+ $cache = $self->{realtime_cache};
+ }
+ $opt{encoding} //= 'ISO-8859-15';
if ( my $content = $cache->thaw($url) ) {
return $promise->resolve($content);
@@ -48,241 +55,260 @@ sub get_polyline_p {
if ( my $err = $tx->error ) {
$promise->reject(
-"hafas->get_polyline_p($url) returned HTTP $err->{code} $err->{message}"
+"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
);
return;
}
- my $body = decode( 'utf-8', $tx->res->body );
- my $json = JSON->new->decode($body);
- my @station_list;
- my @coordinate_list;
-
- for my $feature ( @{ $json->{polyline}{features} } ) {
- if ( exists $feature->{geometry}{coordinates} ) {
- my $coord = $feature->{geometry}{coordinates};
- if ( exists $feature->{properties}{type}
- and $feature->{properties}{type} eq 'stop' )
- {
- push( @{$coord}, $feature->{properties}{id} );
- push( @station_list, $feature->{properties}{name} );
- }
- push( @coordinate_list, $coord );
- }
- }
+ my $body = decode( $opt{encoding}, $tx->res->body );
- my $ret = {
- name => $json->{line}{name} // '?',
- polyline => [@coordinate_list],
- raw => $json,
- };
-
- $cache->freeze( $url, $ret );
-
- # borders ("(Gr)" as in "Grenze") are only returned by HAFAS.
- # They are not stations.
- my $iris_stations = join( '|', $train->route );
- my $hafas_stations
- = join( '|', grep { $_ !~ m{\(Gr\)$} } @station_list );
-
- # Do not return polyline if it belongs to an entirely different
- # train. Trains with longer routes (e.g. due to train number
- # changes, which are handled by HAFAS but left out in IRIS)
- # are okay though.
- if ( $iris_stations ne $hafas_stations
- and index( $hafas_stations, $iris_stations ) == -1 )
- {
- $self->{log}->info( 'Ignoring polyline for '
- . $train->line
- . ": IRIS route does not agree with HAFAS route: $iris_stations != $hafas_stations"
- );
- $promise->reject(
- "hafas->get_polyline_p($url): polyline route mismatch");
- }
- else {
- $promise->resolve($ret);
- }
+ $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) = @_;
- $promise->reject("hafas->get_polyline_p($url): $err");
+ $self->{log}->info("hafas->get_json_p($url): $err");
+ $promise->reject("hafas->get_json_p($url): $err");
return;
}
)->wait;
-
return $promise;
}
-sub get_json_p {
- my ( $self, $url ) = @_;
+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),
+ );
+}
- my $cache = $self->{main_cache};
- my $promise = Mojo::Promise->new;
+sub search_location_p {
+ my ( $self, %opt ) = @_;
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
- }
+ 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),
+ );
+}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
+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 ($tx) = @_;
+ my ($hafas) = @_;
+ my @results = $hafas->results;
- if ( my $err = $tx->error ) {
+ if ( not @results ) {
$promise->reject(
-"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
- );
+ "journeyMatch($train_desc) returned no results");
return;
}
- my $body = decode( 'ISO-8859-15', $tx->res->body );
+ my $result = $results[0];
+ if ( @results > 1 ) {
+ for my $journey (@results) {
+ if ( ( $journey->route )[0]->loc->name eq $train->origin ) {
+ $result = $journey;
+ last;
+ }
+ }
+ }
- $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);
+ $promise->resolve( $result->id );
return;
}
)->catch(
sub {
my ($err) = @_;
- $self->{log}->info("hafas->get_json_p($url): $err");
- $promise->reject("hafas->get_json_p($url): $err");
+ $promise->reject($err);
return;
}
)->wait;
+
return $promise;
}
-sub get_xml_p {
- my ( $self, $url ) = @_;
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
- my $cache = $self->{realtime_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(
+ 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 ($tx) = @_;
+ my ($hafas) = @_;
+ my $journey = $hafas->result;
- if ( my $err = $tx->error ) {
- $promise->reject(
-"hafas->get_xml_p($url) returned HTTP $err->{code} $err->{message}"
- );
+ if ($journey) {
+ $promise->resolve($journey);
return;
}
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
- my $tree;
-
- my $traininfo = {
- station => {},
- messages => [],
- };
-
- # <SDay text="... &gt; ..."> is invalid XML, but present in
- # regardless. As it is the last tag, we just throw it away.
- $body =~ s{<SDay [^>]*/>}{}s;
-
- # More fixes for invalid XML
- $body =~ s{P&R}{P&amp;R};
- $body =~ s{Wagen \d+ \K&}{&amp;};
- $body =~ s{Wagen \d+, \d+ \K&}{&amp;};
-
- # <Attribute [...] text="[...]"[...]"" /> is invalid XML.
- # Work around it.
- $body
- =~ s{<Attribute([^>]+)text="([^"]*)"([^"=>]*)""}{<Attribute$1text="$2&#042;$3&#042;"}s;
-
- # Same for <HIMMessage lead="[...]"[...]"[...]" />
- $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)"([^"=>]*)"([^"]*)"}{<Attribute$1text="$2&#042;$3&#042;$4"}s;
-
- # ... and <HIMMessage [...] lead="[...]<>[...]">
- # (replace <> with t$t)
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<>([^"=]*)"}{<HIMMessage$1lead="$2&#11020;$3"}gis
- )
- {
- }
-
- # Dito for <HIMMessage [...] lead="[...]<br>[...]">.
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<br/?>([^"=]*)"}{<HIMMessage$1lead="$2 $3"}is
- )
- {
- }
-
- # ... and any other HTML tag inside an XML attribute
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<[^>]+>([^"=]*)"}{<HIMMessage$1lead="$2$3"}is
- )
- {
- }
+ return $promise;
+}
- eval { $tree = XML::LibXML->load_xml( string => $body ) };
- if ( my $err = $@ ) {
- if ( $err =~ m{extra content at the end}i ) {
+sub get_route_timestamps_p {
+ my ( $self, %opt ) = @_;
- # We requested XML, but received an HTML error page
- # (which was returned with HTTP 200 OK).
- $self->{log}->debug("load_xml($url): $err");
+ 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;
}
- else {
- # There is invalid XML which we might be able to fix via
- # regular expressions, so dump it into the production log.
- $self->{log}->info("load_xml($url): $err");
+ if ( ( $stop->arr_cancelled or not $stop->sched_arr )
+ and ( $stop->dep_cancelled or not $stop->sched_dep ) )
+ {
+ $ret->{$name}{isCancelled} = 1;
}
- $cache->freeze( $url, $traininfo );
- $promise->reject("hafas->get_xml_p($url): $err");
- return;
+ 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;
}
- 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,
- };
- }
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
- 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
+ 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"
+ );
+ }
}
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
+ $promise->resolve( $ret, $journey, $polyline );
return;
}
)->catch(
sub {
my ($err) = @_;
- $self->{log}->info("hafas->get_xml_p($url): $err");
- $promise->reject("hafas->get_xml_p($url): $err");
+ $promise->reject($err);
return;
}
)->wait;
+
return $promise;
}
diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm
index 3c4fba1..deed79a 100644
--- a/lib/Travelynx/Helper/IRIS.pm
+++ b/lib/Travelynx/Helper/IRIS.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::IRIS;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -10,7 +10,10 @@ 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 ) = @_;
@@ -21,10 +24,20 @@ sub new {
sub get_departures {
my ( $self, %opt ) = @_;
my $station = $opt{station};
- my $lookbehind = $opt{lookbehind} // 180;
- my $lookahead = $opt{lookahead} // 30;
+ 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);
@@ -48,8 +61,8 @@ sub get_departures {
with_related => $with_related,
);
return {
- results => [ $status->results ],
- errstr => $status->errstr,
+ results => [ $status->results ],
+ errstr => $status->errstr,
station_ds100 =>
( $status->station ? $status->station->{ds100} : undef ),
station_eva =>
@@ -62,7 +75,8 @@ sub get_departures {
elsif ( @station_matches > 1 ) {
return {
results => [],
- errstr => 'Mehrdeutiger Stationsname. Mögliche Eingaben: '
+ errstr =>
+ "Mehrdeutiger Stationsname: '$station'. Mögliche Eingaben: "
. join( q{, }, map { $_->[1] } @station_matches ),
};
}
@@ -74,6 +88,111 @@ sub get_departures {
}
}
+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;
@@ -85,27 +204,31 @@ sub route_diff {
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 ] );
+ 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' ], );
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
$route_idx++;
}
else {
- push( @json_route, [ $sched_route[$sched_idx], {}, 'cancelled' ], );
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
$sched_idx++;
}
}
while ( $route_idx <= $#route ) {
- push( @json_route, [ $route[$route_idx], {}, 'additional' ], );
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
$route_idx++;
}
while ( $sched_idx <= $#sched_route ) {
- push( @json_route, [ $sched_route[$sched_idx], {}, 'cancelled' ], );
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
$sched_idx++;
}
return @json_route;
diff --git a/lib/Travelynx/Helper/Sendmail.pm b/lib/Travelynx/Helper/Sendmail.pm
index 8a7b1f1..baa1156 100644
--- a/lib/Travelynx/Helper/Sendmail.pm
+++ b/lib/Travelynx/Helper/Sendmail.pm
@@ -1,5 +1,6 @@
package Travelynx::Helper::Sendmail;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -41,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
index 88b91a0..d688004 100644
--- a/lib/Travelynx/Helper/Traewelling.pm
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -1,12 +1,14 @@
package Travelynx::Helper::Traewelling;
-# Copyright (C) 2020 Daniel Friesel
+# 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;
@@ -74,54 +76,61 @@ sub get_status_p {
};
$self->{user_agent}->request_timeout(20)
- ->get_p( "https://traewelling.de/api/v0/user/${username}" => $header )
- ->then(
+ ->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 = "HTTP $err->{code} $err->{message}";
- $promise->reject($err_msg);
+ 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->{statuses}{data}[0] ) {
+ if ( my $status = $tx->result->json->{data}[0] ) {
my $status_id = $status->{id};
my $message = $status->{body};
my $checkin_at
- = $self->parse_datetime( $status->{created_at} );
+ = $self->parse_datetime( $status->{createdAt} );
my $dep_dt = $self->parse_datetime(
- $status->{train_checkin}{departure} );
+ $status->{train}{origin}{departurePlanned} );
my $arr_dt = $self->parse_datetime(
- $status->{train_checkin}{arrival} );
+ $status->{train}{destination}{arrivalPlanned} );
my $dep_eva
- = $status->{train_checkin}{origin}{ibnr};
+ = $status->{train}{origin}{evaIdentifier};
my $arr_eva
- = $status->{train_checkin}{destination}{ibnr};
+ = $status->{train}{destination}{evaIdentifier};
+
+ my $dep_ds100
+ = $status->{train}{origin}{rilIdentifier};
+ my $arr_ds100
+ = $status->{train}{destination}{rilIdentifier};
my $dep_name
- = $status->{train_checkin}{origin}{name};
+ = $status->{train}{origin}{name};
my $arr_name
- = $status->{train_checkin}{destination}{name};
-
- my $category
- = $status->{train_checkin}{hafas_trip}{category};
- my $trip_id
- = $status->{train_checkin}{hafas_trip}{trip_id};
- my $linename
- = $status->{train_checkin}{hafas_trip}{linename};
+ = $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,
@@ -133,7 +142,8 @@ sub get_status_p {
return;
}
else {
- $promise->reject("unknown error");
+ $promise->reject(
+ { text => "v1/${username}/statuses: unknown error" } );
return;
}
}
@@ -141,7 +151,7 @@ sub get_status_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject($err);
+ $promise->reject( { text => "v1/${username}/statuses: $err" } );
return;
}
)->wait;
@@ -160,21 +170,20 @@ sub get_user_p {
};
my $promise = Mojo::Promise->new;
- $ua->get_p( "https://traewelling.de/api/v0/getuser" => $header )->then(
+ $ua->get_p( "https://traewelling.de/api/v1/auth/user" => $header )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- my $err_msg
- = "HTTP $err->{code} $err->{message} bei Abfrage der Nutzerdaten";
+ my $err_msg = "v1/auth/user: HTTP $err->{code} $err->{message}";
$promise->reject($err_msg);
return;
}
else {
- my $user_data = $tx->result->json;
+ my $user_data = $tx->result->json->{data};
$self->{model}->set_user(
uid => $uid,
trwl_id => $user_data->{id},
- screen_name => $user_data->{name},
+ screen_name => $user_data->{displayName},
user_name => $user_data->{username},
);
$promise->resolve;
@@ -184,84 +193,7 @@ sub get_user_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject("$err bei Abfrage der Nutzerdaten");
- return;
- }
- )->wait;
-
- return $promise;
-}
-
-sub login_p {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid};
- my $email = $opt{email};
- my $password = $opt{password};
-
- my $ua = $self->{user_agent}->request_timeout(20);
-
- my $request = {
- email => $email,
- password => $password,
- };
-
- my $promise = Mojo::Promise->new;
- my $token;
-
- $ua->post_p(
- "https://traewelling.de/api/v0/auth/login" => $self->{header},
- json => $request
- )->then(
- sub {
- my ($tx) = @_;
- if ( my $err = $tx->error ) {
- my $err_msg = "HTTP $err->{code} $err->{message} bei Login";
- $promise->reject($err_msg);
- return;
- }
- else {
- my $res = $tx->result->json;
- $token = $res->{token};
- my $expiry_dt = $self->parse_datetime( $res->{expires_at} );
-
- # Fall back to one year expiry
- $expiry_dt //= DateTime->now( time_zone => 'Europe/Berlin' )
- ->add( years => 1 );
- $self->{model}->link(
- uid => $uid,
- email => $email,
- token => $token,
- expires => $expiry_dt
- );
- return $self->get_user_p( $uid, $token );
- }
- }
- )->then(
- sub {
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- if ($token) {
-
- # We have a token, but couldn't complete the login. For now, we
- # solve this by logging out and invalidating the token.
- $self->logout_p(
- uid => $uid,
- token => $token
- )->finally(
- sub {
- $promise->reject($err);
- return;
- }
- );
- }
- else {
- $promise->reject($err);
- }
+ $promise->reject("v1/auth/user: $err");
return;
}
)->wait;
@@ -289,12 +221,13 @@ sub logout_p {
my $promise = Mojo::Promise->new;
$ua->post_p(
- "https://traewelling.de/api/v0/auth/logout" => $header => json =>
+ "https://traewelling.de/api/v1/auth/logout" => $header => json =>
$request )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- my $err_msg = "HTTP $err->{code} $err->{message}";
+ my $err_msg
+ = "v1/auth/logout: HTTP $err->{code} $err->{message}";
$promise->reject($err_msg);
return;
}
@@ -306,7 +239,7 @@ sub logout_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject($err);
+ $promise->reject("v1/auth/logout: $err");
return;
}
)->wait;
@@ -314,7 +247,34 @@ sub logout_p {
return $promise;
}
-sub checkin {
+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 = {
@@ -334,47 +294,63 @@ sub checkin {
}
my $request = {
- tripID => $opt{trip_id},
+ 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,
+ 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/v0/trains/checkin" =>
- $header => json => $request )->then(
+ ->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 ( $err->{code} != 409 and $err->{code} != 406 ) {
- $self->{log}->warn("Traewelling checkin error: $err_msg");
- }
- else {
- $self->{log}->debug("Traewelling checkin error: $err_msg");
+ 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},
+ uid => $opt{uid},
message =>
- "Fehler bei $opt{train_type} $opt{train_no}: $err_msg",
+"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 );
- # As of 2020-10-04, traewelling.de checkins do not yet return
- # "statusId". The patch is present on the develop branch and waiting
- # for a merge into master.
$self->{model}->log(
uid => $opt{uid},
message => "Eingecheckt in $opt{train_type} $opt{train_no}",
@@ -384,21 +360,28 @@ sub checkin {
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("... error: $err");
+ $self->{log}->debug("... $debug_prefix error: $err");
$self->{model}->log(
- uid => $opt{uid},
- message => "Fehler bei $opt{train_type} $opt{train_no}: $err",
+ 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
index 99a88bf..69026ac 100644
--- a/lib/Travelynx/Model/InTransit.pm
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -1,5 +1,6 @@
package Travelynx::Model::InTransit;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -10,45 +11,164 @@ 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;
- $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 ]
- )
+ 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 {
@@ -69,6 +189,148 @@ sub delete {
$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 ) = @_;
@@ -82,11 +344,68 @@ sub get {
}
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 $res->expand->hash;
+ return map { $self->postprocess($_) } $res->expand->hashes->each;
}
- return $res->hash;
+ 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 {
@@ -140,6 +459,12 @@ sub set_arrival {
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(
@@ -192,6 +517,61 @@ sub set_arrival_times {
);
}
+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 ) = @_;
@@ -225,6 +605,7 @@ sub set_route_data {
$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',
{
@@ -252,11 +633,210 @@ sub unset_arrival_data {
);
}
+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 $db = $opt{db} // $self->{pg}->db;
my $new_data = $opt{data} // {};
my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
@@ -279,7 +859,7 @@ sub update_user_data {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
- my $db = $opt{db} // $self->{pg}->db;
+ 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 } )
@@ -298,4 +878,23 @@ sub update_user_data {
);
}
+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
index 89f6051..d23eb04 100755
--- a/lib/Travelynx/Model/JourneyStatsCache.pm
+++ b/lib/Travelynx/Model/JourneyStatsCache.pm
@@ -1,6 +1,6 @@
package Travelynx::Model::JourneyStatsCache;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index a0981c6..97c4681 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -1,12 +1,11 @@
package Travelynx::Model::Journeys;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
-use Geo::Distance;
+use GIS::Distance;
use List::MoreUtils qw(after_incl before_incl);
-use Travel::Status::DE::IRIS::Stations;
use strict;
use warnings;
@@ -16,6 +15,22 @@ 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)
@@ -35,54 +50,57 @@ sub epoch_to_dt {
);
}
-sub get_station {
- my ( $station_name, $exact_match ) = @_;
+sub min_to_human {
+ my ( $self, $minutes ) = @_;
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
+ my @ret;
- if ( @candidates == 1 ) {
- if ( not $exact_match ) {
- return $candidates[0];
- }
- if ( $candidates[0][0] eq $station_name
- or $candidates[0][1] eq $station_name
- or $candidates[0][2] eq $station_name )
- {
- return $candidates[0];
- }
- return undef;
+ if ( $minutes >= 14 * 24 * 60 ) {
+ push( @ret, int( $minutes / ( 7 * 24 * 60 ) ) . ' Wochen' );
}
- return undef;
-}
+ elsif ( $minutes >= 7 * 24 * 60 ) {
+ push( @ret, '1 Woche' );
+ }
+ $minutes %= 7 * 24 * 60;
-sub grep_unknown_stations {
- my (@stations) = @_;
+ if ( $minutes >= 2 * 24 * 60 ) {
+ push( @ret, int( $minutes / ( 24 * 60 ) ) . ' Tage' );
+ }
+ elsif ( $minutes >= 24 * 60 ) {
+ push( @ret, '1 Tag' );
+ }
+ $minutes %= 24 * 60;
- my @unknown_stations;
- for my $station (@stations) {
- my $station_info = get_station($station);
- if ( not $station_info ) {
- push( @unknown_stations, $station );
- }
+ 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" );
}
- return @unknown_stations;
+ 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 ) = @_;
- $opt{journey_edit_mask} = {
- sched_departure => 0x0001,
- real_departure => 0x0002,
- from_station => 0x0004,
- route => 0x0010,
- is_cancelled => 0x0020,
- sched_arrival => 0x0100,
- real_arrival => 0x0200,
- to_station => 0x0400,
- };
-
return bless( \%opt, $class );
}
@@ -100,8 +118,8 @@ sub add {
my $db = $opt{db};
my $uid = $opt{uid};
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 $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' );
@@ -134,10 +152,14 @@ sub add {
my $route_has_stop = 0;
for my $station ( @{ $opt{route} || [] } ) {
- if ( $station eq $dep_station->[1] or $station eq $dep_station->[0] ) {
+ if ( $station eq $dep_station->{name}
+ or $station eq $dep_station->{ds100} )
+ {
$route_has_start = 1;
}
- if ( $station eq $arr_station->[1] or $station eq $arr_station->[0] ) {
+ if ( $station eq $arr_station->{name}
+ or $station eq $arr_station->{ds100} )
+ {
$route_has_stop = 1;
}
}
@@ -145,18 +167,19 @@ sub add {
my @route;
if ( not $route_has_start ) {
- push( @route, [ $dep_station->[1], {}, undef ] );
+ push( @route, [ $dep_station->{name}, $dep_station->{eva}, {} ] );
}
if ( $opt{route} ) {
my @unknown_stations;
for my $station ( @{ $opt{route} } ) {
- my $station_info = get_station($station);
+ my $station_info = $self->{stations}->search($station);
if ($station_info) {
- push( @route, [ $station_info->[1], {}, undef ] );
+ push( @route,
+ [ $station_info->{name}, $station_info->{eva}, {} ] );
}
else {
- push( @route, [ $station, {}, undef ] );
+ push( @route, [ $station, undef, {} ] );
push( @unknown_stations, $station );
}
}
@@ -175,7 +198,7 @@ sub add {
}
if ( not $route_has_stop ) {
- push( @route, [ $arr_station->[1], {}, undef ] );
+ push( @route, [ $arr_station->{name}, $arr_station->{eva}, {} ] );
}
my $entry = {
@@ -184,11 +207,11 @@ sub add {
train_line => $opt{train_line},
train_no => $opt{train_no},
train_id => 'manual',
- checkin_station_id => $dep_station->[2],
+ checkin_station_id => $dep_station->{eva},
checkin_time => $now,
sched_departure => $opt{sched_departure},
real_departure => $opt{rt_departure},
- checkout_station_id => $arr_station->[2],
+ checkout_station_id => $arr_station->{eva},
sched_arrival => $opt{sched_arrival},
real_arrival => $opt{rt_arrival},
checkout_time => $now,
@@ -231,7 +254,8 @@ sub add_from_in_transit {
$journey->{edited} = 0;
$journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' );
- $db->insert( 'journeys', $journey );
+ return $db->insert( 'journeys', $journey, { returning => 'id' } )
+ ->hash->{id};
}
sub update {
@@ -252,14 +276,14 @@ sub update {
eval {
if ( exists $opt{from_name} ) {
- my $from_station = get_station( $opt{from_name}, 1 );
+ 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->[2],
+ checkin_station_id => $from_station->{eva},
edited => $journey->{edited} | 0x0004,
},
{
@@ -268,14 +292,14 @@ sub update {
)->rows;
}
if ( exists $opt{to_name} ) {
- my $to_station = get_station( $opt{to_name}, 1 );
+ 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->[2],
+ checkout_station_id => $to_station->{eva},
edited => $journey->{edited} | 0x0400,
},
{
@@ -341,7 +365,7 @@ sub update {
)->rows;
}
if ( exists $opt{route} ) {
- my @new_route = map { [ $_, {}, undef ] } @{ $opt{route} };
+ my @new_route = map { [ $_, undef, {} ] } @{ $opt{route} };
$rows = $db->update(
'journeys',
{
@@ -491,7 +515,7 @@ sub get {
my @select
= (
- qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva checkout_ts sched_arr_ts real_arr_ts arr_eva cancelled edited route messages user_data)
+ 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,
@@ -511,6 +535,10 @@ sub get {
$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};
@@ -519,11 +547,24 @@ sub get {
$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 );
@@ -531,35 +572,43 @@ sub get {
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},
+ 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_polyline} ) {
- $ref->{polyline} = $entry->{polyline};
+ if ( $opt{with_visibility} ) {
+ $ref->{visibility_str}
+ = $ref->{visibility}
+ ? $visibility_itoa{ $ref->{visibility} }
+ : 'default';
+ $ref->{effective_visibility_str}
+ = $visibility_itoa{ $ref->{effective_visibility} };
}
- if ( my $station = $self->{station_by_eva}->{ $ref->{from_eva} } ) {
- $ref->{from_ds100} = $station->[0];
- $ref->{from_name} = $station->[1];
- }
- if ( my $station = $self->{station_by_eva}->{ $ref->{to_eva} } ) {
- $ref->{to_ds100} = $station->[0];
- $ref->{to_name} = $station->[1];
+ if ( $opt{with_polyline} ) {
+ $ref->{polyline} = $entry->{polyline};
}
if ( $opt{with_datetime} ) {
@@ -570,11 +619,23 @@ sub get {
$ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} );
$ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} );
$ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} );
+ 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] };
}
@@ -643,11 +704,20 @@ sub get_latest {
cancelled => 0
},
{
- order_by => { -desc => 'journey_id' },
+ 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',
'*',
@@ -660,6 +730,15 @@ sub get_latest {
}
)->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 );
}
@@ -713,6 +792,45 @@ sub get_latest_checkout_station_id {
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 ) = @_;
@@ -904,35 +1022,36 @@ sub sanity_check {
and $journey->{sched_duration} <= 0 )
{
return
-'Die geplante Dauer dieser Zugfahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+'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 Zugfahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+'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 Zugfahrt ist länger als 24 Stunden.';
+ return 'Die Fahrt 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.';
+ return 'Die Fahrt 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.';
+ return 'Fahrten mit über 500 km/h? Schön wär\'s.';
}
- if ( $journey->{route} and @{ $journey->{route} } > 99 ) {
+ if ( $journey->{route} and @{ $journey->{route} } > 199 ) {
my $stop_count = @{ $journey->{route} };
return
-"Die Zugfahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
+"Die Fahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
}
if ( $journey->{edited} & 0x0010 and not $lax ) {
my @unknown_stations
- = grep_unknown_stations( map { $_->[0] } @{ $journey->{route} } );
+ = $self->{stations}
+ ->grep_unknown( map { $_->[0] } @{ $journey->{route} } );
if (@unknown_stations) {
return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );
}
@@ -946,16 +1065,28 @@ sub get_travel_distance {
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 = Geo::Distance->new();
+ my $geo = GIS::Distance->new();
my @stations = map { $_->[0] } @{$route_ref};
my @route = after_incl { $_ eq $from } @stations;
@route = before_incl { $_ eq $to } @route;
@@ -973,58 +1104,400 @@ sub get_travel_distance {
my $prev_station = shift @polyline;
for my $station (@polyline) {
-
- #lonlatlonlat
- $distance_polyline
- += $geo->distance( 'kilometer', $prev_station->[0],
- $prev_station->[1], $station->[0], $station->[1] );
+ $distance_polyline += $geo->distance_metal(
+ $prev_station->[1], $prev_station->[0],
+ $station->[1], $station->[0]
+ );
$prev_station = $station;
}
- $prev_station = get_station( shift @route );
+ $prev_station = $self->{latlon_by_station}->{ shift @route };
if ( not $prev_station ) {
return ( $distance_polyline, 0, 0 );
}
- # 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;
-
- # $#{$station} >= 4 iff $station has geocoordinates
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 ( 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 ( $#{$station} >= 4 ) {
- $to_station_beeline = $station;
+ if ( not $shortest_t
+ or $journey->{rt_duration} < $shortest_t->{rt_duration} )
+ {
+ $shortest_t = $journey;
}
- if ( $#{$prev_station} >= 4 and $#{$station} >= 4 ) {
- $distance_intermediate
- += $geo->distance( 'kilometer', $prev_station->[3],
- $prev_station->[4], $station->[3], $station->[4] );
+ }
+
+ if ( $journey->{km_route} ) {
+ if ( not $longest_km
+ or $journey->{km_route} > $longest_km->{km_route} )
+ {
+ $longest_km = $journey;
}
- else {
- $skipped++;
+ 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;
}
- $prev_station = $station;
}
}
- 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]
- );
+ 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};
}
- return ( $distance_polyline, $distance_intermediate,
- $distance_beeline, $skipped );
+ 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 {
@@ -1041,6 +1514,8 @@ sub compute_stats {
my @inconsistencies;
my $next_departure = 0;
+ my $next_id;
+ my $next_train;
for my $journey (@journeys) {
$num_trains++;
@@ -1069,8 +1544,24 @@ sub compute_stats {
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') );
+ 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
@@ -1081,6 +1572,9 @@ sub compute_stats {
$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,
@@ -1120,8 +1614,8 @@ sub get_stats {
}
my $uid = $opt{uid};
- my $db = $opt{db} // $self->{pg}->db;
- my $year = $opt{year} // 0;
+ 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.
@@ -1129,7 +1623,8 @@ sub get_stats {
# checks out of a train or manually edits/adds a journey.
if (
- not $opt{write_only}
+ not $opt{write_only}
+ and not $opt{review}
and my $stats = $self->stats_cache->get(
uid => $uid,
db => $db,
@@ -1168,7 +1663,7 @@ sub get_stats {
my @journeys = $self->get(
uid => $uid,
- cancelled => $opt{cancelled} ? 1 : 0,
+ cancelled => 0,
verbose => 1,
with_polyline => 1,
after => $interval_start,
@@ -1184,7 +1679,107 @@ sub get_stats {
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
index a334c1d..25648cc 100644
--- a/lib/Travelynx/Model/Traewelling.pm
+++ b/lib/Travelynx/Model/Traewelling.pm
@@ -1,6 +1,6 @@
package Travelynx::Model::Traewelling;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -38,20 +38,18 @@ sub now {
sub link {
my ( $self, %opt ) = @_;
- my $log = [ [ $self->now->epoch, "Erfolgreich angemeldet" ] ];
+ my $log = [ [ $self->now->epoch, "Erfolgreich mittels OAuth2 verbunden" ] ];
- my $data = {
- log => $log,
- expires => $opt{expires}->epoch,
- };
+ my $data = { log => $log };
my $user_entry = {
- user_id => $opt{uid},
- email => $opt{email},
- push_sync => 0,
- pull_sync => 0,
- token => $opt{token},
- data => JSON->new->encode($data),
+ 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(
@@ -59,7 +57,7 @@ sub link {
$user_entry,
{
on_conflict => \
-'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null'
+'(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'
}
);
@@ -94,20 +92,24 @@ sub unlink {
}
sub get {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id};
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
my $res_h
- = $self->{pg}->db->select( 'traewelling_str', '*', { user_id => $uid } )
+ = $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->{data}{expires} );
+ $res_h->{expires_on}
+ = epoch_to_dt( $res_h->{expiry_ts} // $res_h->{data}{expires} );
- my $expires_in = ( $res_h->{data}{expires} // 0 ) - $self->now->epoch;
+ my $expires_in = ( $res_h->{expiry_ts} // $res_h->{data}{expires} // 0 )
+ - $self->now->epoch;
if ( $expires_in < 0 ) {
$res_h->{expired} = 1;
@@ -211,16 +213,17 @@ sub get_pushable_accounts {
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.checkin_station_id as dep_eva, i.checkout_station_id as arr_eva,
+ 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,
- extract(epoch from i.checkin_time) as checkin_ts,
- extract(epoch from i.sched_departure) as dep_ts,
- extract(epoch from i.sched_arrival) as arr_ts
+ 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 as i on t.user_id = i.user_id
+ join in_transit_str as i on t.user_id = i.user_id
where t.push_sync = True
- and i.checkout_station_id is not null
+ and i.arr_eva is not null
and i.cancelled = False
}
);
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index 535b938..4602fa2 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -1,6 +1,6 @@
package Travelynx::Model::Users;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -8,7 +8,53 @@ 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 ) = @_;
@@ -16,6 +62,20 @@ sub new {
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};
@@ -23,8 +83,25 @@ sub mark_seen {
$db->update(
'users',
- { last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) },
- { id => $uid }
+ {
+ 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 }
);
}
@@ -34,7 +111,10 @@ sub verify_registration_token {
my $token = $opt{token};
my $db = $opt{db} // $self->{pg}->db;
- my $tx = $db->begin;
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
my $res = $db->select(
'pending_registrations',
@@ -48,12 +128,30 @@ sub verify_registration_token {
if ( $res->hash->{count} ) {
$db->update( 'users', { status => 1 }, { id => $uid } );
$db->delete( 'pending_registrations', { user_id => $uid } );
- $tx->commit;
+ 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;
@@ -76,22 +174,41 @@ sub get_uid_by_name_and_mail {
return;
}
-sub get_privacy_by_name {
+sub get_privacy_by {
my ( $self, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
- my $name = $opt{name};
+ 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', 'public_level' ],
- {
- name => $name,
- status => 1
- }
+ [ 'id', 'name', 'public_level', 'accept_follows' ],
+ { %where, status => 1 }
);
if ( my $user = $res->hash ) {
- return $user;
+ 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;
}
@@ -102,9 +219,39 @@ sub set_privacy {
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;
@@ -256,7 +403,7 @@ sub remove_password_token {
);
}
-sub get_data {
+sub get {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
my $uid = $opt{uid};
@@ -264,6 +411,7 @@ sub get_data {
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',
@@ -271,11 +419,28 @@ sub get_data {
)->hash;
if ($user) {
return {
- id => $user->{id},
- name => $user->{name},
- status => $user->{status},
- is_public => $user->{public_level},
- email => $user->{email},
+ 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'
@@ -309,13 +474,13 @@ sub get_login_data {
return $res_h;
}
-sub add_user {
+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 = $opt{password_hash};
+ 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
@@ -328,9 +493,10 @@ sub add_user {
my $res = $db->insert(
'users',
{
- name => $user_name,
- status => 0,
- public_level => 0,
+ name => $user_name,
+ status => 0,
+ public_level => $visibility_atoi{unlisted}
+ | ( $visibility_atoi{unlisted} << 8 ),
email => $email,
password => $password,
registered_at => $now,
@@ -383,11 +549,49 @@ sub unflag_deletion {
);
}
-sub set_password_hash {
+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 = $opt{password_hash};
+ my $password = $self->hash_password( $opt{password} );
$db->update( 'users', { password => $password }, { id => $uid } );
}
@@ -455,4 +659,502 @@ sub use_history {
}
}
+sub use_external_services {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ my $uid = $opt{uid};
+ my $value = $opt{set};
+
+ if ( defined $value ) {
+ if ( $value < 0 or $value > 4 ) {
+ $value = 0;
+ }
+ $db->update( 'users', { external_services => $value }, { id => $uid } );
+ }
+ else {
+ return $db->select( 'users', ['external_services'], { id => $uid } )
+ ->hash->{external_services};
+ }
+}
+
+sub get_webhook {
+ my ( $self, %opt ) = @_;
+ my $db = $opt{db} // $self->{pg}->db;
+ 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 5685548..b564d65 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,19 +1,18 @@
-const CACHE_NAME = 'static-cache-v38';
+const CACHE_NAME = 'static-cache-v72';
const FILES_TO_CACHE = [
'/favicon.ico',
'/offline.html',
- '/static/v38/css/light.min.css',
- '/static/v38/css/dark.min.css',
- '/static/v38/css/material-icons.css',
- '/static/v38/css/local.css',
- '/static/v38/fonts/MaterialIcons-Regular.woff2',
- '/static/v38/fonts/MaterialIcons-Regular.woff',
- '/static/v38/fonts/MaterialIcons-Regular.ttf',
- '/static/v38/js/jquery-3.4.1.min.js',
- '/static/v38/js/materialize.min.js',
- '/static/v38/js/travelynx-actions.min.js',
- '/static/v38/js/autocomplete.min.js',
- '/static/v38/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
index a289cae..e55c5f7 100644
--- a/public/static/api.yml
+++ b/public/static/api.yml
@@ -1,4 +1,4 @@
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020 Birte Kristina Friesel
#
# SPDX-License-Identifier: CC0-1.0
openapi: 3.0.3
diff --git a/public/static/css/dark.min.css b/public/static/css/dark.min.css
index 602df0b..3594ca3 100644
--- a/public/static/css/dark.min.css
+++ b/public/static/css/dark.min.css
@@ -1,8 +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: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}/*!
+.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:#fff}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 #fff;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 #fff;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:rgba(255,255,255,0.5);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}.pagination li a{color:#fff}.collection-item{color:#fff}.collection-item .secondary-content{color:#fff}.collection-item.disabled{color:#757575}.progress{background-color:#424242}.progress>.determinate{background-color:#2196F3}html{background-color:#101010}input[type=email],input[type=text],input[type=password],textarea{color:#fff}
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #98f5ff}button:focus{outline:none;background-color:#00a9bb}label{font-size:.8rem;color:#fff}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #0097a7;box-shadow:0 1px 0 0 #0097a7}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#0097a7}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#388E3C}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#D32F2F}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #388E3C;box-shadow:0 1px 0 0 #388E3C}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #D32F2F;box-shadow:0 1px 0 0 #D32F2F}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#388E3C}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#D32F2F}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#fff;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#0097a7}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #fff}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #0097a7}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#0097a7}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #fff;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #0097a7;border-bottom:2px solid #0097a7;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #0097a7;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #fff;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #0097a7;background-color:#0097a7;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#fff;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#0097a7;border-color:#0097a7}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#42d5e4}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#0097a7}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(0,151,167,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,151,167,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #0097a7}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#0097a7;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#0097a7;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;-webkit-appearance:none;background-color:#0097a7;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #8b1014}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #8b1014}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#fff}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#00aec1}.sidenav li>a.btn-floating:hover{background-color:#0097a7}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#8b1014}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#0097a7}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#757575;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#8b1014;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#0097a7;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#0097a7}.datepicker-table td.is-selected{background-color:#0097a7;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(4,148,163,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#0097a7;padding:0 1rem}.datepicker-clear{color:#D32F2F}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#0097a7;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(0,151,167,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#0097a7;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#0097a7}.timepicker-canvas-bg{stroke:none;fill:#0097a7}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#D32F2F}.timepicker-close{color:#0097a7}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#D32F2F}.caution-color-text{color:#D32F2F}.info-color{background-color:#263238}.info-color-text{color:#263238}.contrast-color{background-color:#fff}.contrast-color-text{color:#fff}.success-color{background-color:#388E3C}.success-color-text{color:#388E3C}html{background-color:#101010}a.unmarked{color:#fff}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:#fff}.collection-item{color:#fff}.collection-item .secondary-content{color:#fff}.collection-item.disabled{color:#757575}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#212121;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#212121;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#702020;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:#fff}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:#fff}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .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 6e7a5cc..22a7b63 100644
--- a/public/static/css/light.min.css
+++ b/public/static/css/light.min.css
@@ -1,8 +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: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}/*!
+.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:#9e9e9e}::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:#9e9e9e;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:rgba(255,255,255,0.5);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:#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)}.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}.progress{background-color:#e0e0e0}.progress>.determinate{background-color:#2196F3}
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#000}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#4CAF50}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #4CAF50;box-shadow:0 1px 0 0 #4CAF50}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #F44336;box-shadow:0 1px 0 0 #F44336}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#4CAF50}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#F44336}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#000;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#616161;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:rgba(0,0,0,0.87)}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#F44336}.caution-color-text{color:#F44336}.info-color{background-color:#fff9c4}.info-color-text{color:#fff9c4}.contrast-color{background-color:rgba(0,0,0,0.87)}.contrast-color-text{color:rgba(0,0,0,0.87)}.success-color{background-color:#4CAF50}.success-color-text{color:#4CAF50}html{background-color:#fff}a.unmarked{color:rgba(0,0,0,0.87)}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:rgba(0,0,0,0.87)}.collection-item{color:rgba(0,0,0,0.87)}.collection-item .secondary-content{color:rgba(0,0,0,0.87)}.collection-item.disabled{color:#616161}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#eee;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#eee;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#FFCDD2;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:rgba(0,0,0,0.87)}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:rgba(0,0,0,0.87)}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .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 5449a23..0000000
--- a/public/static/css/local.css
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2020 Daniel Friesel
- *
- * SPDX-License-Identifier: MIT
- */
-.action-checkin,
-.action-checkout,
-.action-undo,
-.action-cancelled-from,
-.action-cancelled-to,
-.action-share {
- cursor: pointer;
-}
-
-.brand-logo span {
- transition: color 1s;
-}
-
-.brand-logo:hover .ca,
-.brand-logo:hover .ce {
- color: #a8e3fa;
-}
-
-.brand-logo:hover .cb,
-.brand-logo:hover .cd {
- color: #f5c4ce;
-}
-
-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 f58f29e..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/v38/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/v38/fonts/MaterialIcons-Regular.woff2) format('woff2'),
- url(/static/v38/fonts/MaterialIcons-Regular.woff) format('woff'),
- url(/static/v38/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/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 a99e240..0000000
--- a/public/static/js/autocomplete.js
+++ /dev/null
@@ -1,8836 +0,0 @@
-/*
- * Copyright (C) 2020 DB Station&Service AG, Europaplatz 1, 10557 Berlin
- * Copyright (C) 2020 Daniel Friesel
- *
- * SPDX-License-Identifier: CC-BY-4.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-Rothe Erde": null,
- "Aalen Hbf": null,
- "Aalten": null,
- "Aalter": null,
- "Aarau": null,
- "Aarburg-Oftringen": null,
- "Aarhus": null,
- "Abcoude": null,
- "Abenden": null,
- "Abensberg": null,
- "Achern": null,
- "Achern Stadt": null,
- "Achiet": 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,
- "Affoltern am Albis": null,
- "Agatharied": null,
- "Agathenburg": null,
- "Agde": 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 (S)": null,
- "Ahrensfelde Friedhof": null,
- "Ahrensfelde Nord": null,
- "Ahrweiler": null,
- "Ahrweiler Markt": null,
- "Aich(Niederbay)": null,
- "Aichach": null,
- "Aichstetten": null,
- "Aigle": null,
- "Aime-la-Plagne": null,
- "Aindorf": null,
- "Ainring": null,
- "Airolo": null,
- "Aix-en-Provence TGV": null,
- "Aix-les-Bains-le-Revard": null,
- "Akkrum": null,
- "Alba Iulia": null,
- "Albate-Camerlata": null,
- "Albbruck": null,
- "Albersdorf": null,
- "Albersweiler(Pfalz)": null,
- "Albertville": null,
- "Albgaubad, Ettlingen": null,
- "Albig": 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,
- "Alençon": null,
- "Ales": null,
- "Aletshausen": null,
- "Alexisbad": null,
- "Alfeld(Leine)": null,
- "Alfter-Impekoven": null,
- "Alfter-Witterschlick": null,
- "Algermissen": null,
- "Aligse": null,
- "Alken(B)": null,
- "Alkmaar": null,
- "Alkmaar Noord": null,
- "Allendorf(Dillkr)": null,
- "Allendorf(Eder) Bf": null,
- "Allensbach": null,
- "Allerheiligenhöfe": null,
- "Allersberg(Rothsee)": null,
- "Allmendingen": null,
- "Almelo": null,
- "Almelo de Riet": null,
- "Almere Buiten": null,
- "Almere Centrum": null,
- "Almere Muziekwijk": null,
- "Almere Oostvaarders": null,
- "Almere Parkwijk": null,
- "Almere Poort": null,
- "Alpen": null,
- "Alphen aan den Rijn": 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,
- "Alt Schwerin": null,
- "Altach": null,
- "Altbach": null,
- "Altdorf West": null,
- "Altdorf(CH)": 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,
- "Altendorf(CH)": null,
- "Altenerding": null,
- "Altenfeld(Rhön)": null,
- "Altenglan": null,
- "Altengörs": null,
- "Altenhasungen": null,
- "Altenkirchen(Westerwald)": null,
- "Altenmarkt im Pongau": 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,
- "Altmarkt/Regierungspräsidium, Kassel": null,
- "Altmittweida": null,
- "Altmorschen": null,
- "Altnau": null,
- "Altomünster": null,
- "Altoschatz-Rosenthal": null,
- "Altranft": null,
- "Altshausen": null,
- "Altstädten(Allgäu)": null,
- "Altstätten SG": null,
- "Alttann": null,
- "Altötting": null,
- "Alveslohe": null,
- "Alvesta station": null,
- "Alzenau Burg": null,
- "Alzenau Nord": null,
- "Alzenau(Unterfr)": null,
- "Alzey": null,
- "Alzey Süd": null,
- "Alzey West": null,
- "Am Kupferhammer, Kassel": null,
- "Am Stern, Kassel": null,
- "Am Weinberg, Kassel": null,
- "Amberg": null,
- "Amberieu": null,
- "Amerang": null,
- "Amersfoort Centraal": null,
- "Amersfoort Schothorst": null,
- "Amersfoort Vathorst": null,
- "Ammern": null,
- "Amorbach": null,
- "Ampfing": null,
- "Amriswil": null,
- "Amsdorf": null,
- "Amsterdam Amstel": null,
- "Amsterdam Bijlmer ArenA": null,
- "Amsterdam Centraal": null,
- "Amsterdam Holendrecht": null,
- "Amsterdam Lelylaan": null,
- "Amsterdam Muiderpoort": null,
- "Amsterdam RAI": null,
- "Amsterdam Science Park": null,
- "Amsterdam Sloterdijk": null,
- "Amsterdam Zuid": null,
- "Amstetten NÖ": null,
- "Amstetten(W) Lokalbahn": null,
- "Amstetten(Württ)": null,
- "Amtshainersdorf": null,
- "Andelfingen": null,
- "Andermatt": null,
- "Andernach": null,
- "Andorf": null,
- "Angermund": null,
- "Angermünde": null,
- "Angern-Rogätz": null,
- "Angersbach": null,
- "Angersdorf": null,
- "Angleur": null,
- "Angouleme": null,
- "Anklam": null,
- "Anna Paulowna": 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,
- "Ans(B)": null,
- "Ansbach": null,
- "Antibes": null,
- "Antonsthal": null,
- "Antwerpen Centraal": null,
- "Antwerpen-Berchem": null,
- "Antwerpen-Zuid": null,
- "Anwanden": null,
- "Anzefahr": null,
- "Anzenkirchen": null,
- "Apach(Moselle)": null,
- "Apeldoorn": null,
- "Apeldoorn De Maten": null,
- "Apeldoorn Osseveld": null,
- "Apensen": null,
- "Apolda": null,
- "Appenweier": null,
- "Appingedam": null,
- "Arad": null,
- "Arbon": null,
- "Arbon (See)": null,
- "Arbste": null,
- "Ardey": null,
- "Arensdorf(Köthen)": null,
- "Arenshausen": null,
- "Arfurt(Lahn)": null,
- "Argeles-sur-Mer": null,
- "Arkel": null,
- "Arles": null,
- "Arlon": null,
- "Armsheim": null,
- "Arnbach": null,
- "Arnemuiden": null,
- "Arnhem Centraal": null,
- "Arnhem Presikhaaf": null,
- "Arnhem Velperpoort": null,
- "Arnhem Zuid": null,
- "Arnoldstein": null,
- "Arnsberg(Westf)": null,
- "Arnschwang": null,
- "Arnsdorf(Dresden)": null,
- "Arnstadt Hbf": null,
- "Arnstadt Süd": null,
- "Arosa": null,
- "Arrach": null,
- "Arras(F)": null,
- "Arsbeck": null,
- "Artenay(Loiret)": null,
- "Artern": null,
- "Arth-Goldau": null,
- "Arvant": null,
- "Arzberg(Oberfr)": null,
- "As(CZ)": 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,
- "Asse": null,
- "Asselheim": null,
- "Assen": null,
- "Assenheim(Oberhess)": null,
- "Assmannshausen": null,
- "Attendorn": null,
- "Attendorn-Hohen Hagen": null,
- "Attnang-Puchheim": null,
- "Au SG": null,
- "Au ZH": 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,
- "Auersmacher": null,
- "Auestadion, Kassel": 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-Oberhausen": null,
- "August-Bebel-Straße, Karlsruhe": null,
- "Augustfehn": null,
- "Augustusburg Bergstation": null,
- "Aukrug": null,
- "Aulendorf": null,
- "Aulnoye Aymeries": null,
- "Aumenau": null,
- "Aumühle": null,
- "Auneau(Dourdan)": null,
- "Auringen-Medenbach": null,
- "Auvelais": null,
- "Auw an der Kyll": null,
- "Außenried": null,
- "Avesnes-sur-Helpe": null,
- "Avignon Centre": null,
- "Avignon TGV": null,
- "Aying": null,
- "Aßlar": null,
- "Aßling(Oberbay)": null,
- "Baabe": null,
- "Baalberge": null,
- "Baar(CH)": null,
- "Baar-Ebenhausen": null,
- "Baarn": null,
- "Babenhausen Langstadt": null,
- "Babenhausen(Hess)": null,
- "Babstadt": null,
- "Babylon": 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 Aussee": 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 Blumau": 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ürkheim": null,
- "Bad Dürkheim-Trift": null,
- "Bad Dürrenberg": null,
- "Bad Elster": 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 Gastein": null,
- "Bad Griesbach(Schwarzwald)": null,
- "Bad Grönenbach": null,
- "Bad Harzburg": null,
- "Bad Herrenalb": null,
- "Bad Hersfeld": null,
- "Bad Hofgastein": null,
- "Bad Holzhausen": null,
- "Bad Homburg": null,
- "Bad Honnef Stadtbahn": null,
- "Bad Honnef(Rhein)": null,
- "Bad Höhenstadt": null,
- "Bad Hönningen": null,
- "Bad Imnau": null,
- "Bad Ischl": 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 Nieuweschans": null,
- "Bad Oeynhausen": null,
- "Bad Oeynhausen Süd": null,
- "Bad Oldesloe": null,
- "Bad Peterstal": null,
- "Bad Pyrmont": null,
- "Bad Ragaz": 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 Salzuflen-Sylbach": null,
- "Bad Salzungen": null,
- "Bad Sassendorf": null,
- "Bad Saulgau": null,
- "Bad Schallerbach-Wallern": null,
- "Bad Schandau": null,
- "Bad Schlema": null,
- "Bad Schmiedeberg": null,
- "Bad Schmiedeberg Kurzentrum": 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 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 Vigaun": 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 Zurzach": null,
- "Bad Zwischenahn": null,
- "Baddeckenstedt": null,
- "Baden(CH)": null,
- "Baden(Verden)": null,
- "Baden-Baden": null,
- "Baden-Baden Haueneberstein": null,
- "Baden-Baden Rebland": null,
- "Baflo": null,
- "Bagenz": null,
- "Bahlingen Riedlen": null,
- "Bahlingen am Kaiserstuhl": null,
- "Bahnbrücken": null,
- "Bahnhof Niederzwehren, Kassel": null,
- "Bahnhof, Gönnheim": null,
- "Bahnsdorf": null,
- "Baierbrunn": null,
- "Baiersbronn Bf": null,
- "Baiersbronn Schule": null,
- "Baiersdorf": null,
- "Baisieux": null,
- "Baitz": null,
- "Balbersdorf": null,
- "Baldham": null,
- "Balduinstein": null,
- "Balerna": null,
- "Balgheim": null,
- "Balgstädt": null,
- "Balingen Süd": null,
- "Balingen(Württ)": null,
- "Ballersbach": null,
- "Ballstädt(Gotha)": null,
- "Baltersweiler": null,
- "Balve": null,
- "Bamberg": null,
- "Bammental": null,
- "Bannemin-Mölschow": null,
- "Banova Jaruga": null,
- "Bansin Seebad": null,
- "Banteln": null,
- "Bantin": null,
- "Bantorf": null,
- "Bantzenheim": null,
- "Banyuls-sur-Mer": null,
- "Bar-le-Duc": null,
- "Barabein": null,
- "Barbelroth": null,
- "Barby": null,
- "Barcelona Sants": null,
- "Barchel": null,
- "Bardowick": null,
- "Barendrecht": null,
- "Bargstedt": null,
- "Bargteheide": null,
- "Barleben": null,
- "Barleber See": null,
- "Barmstedt": null,
- "Barmstedt Brunnenstr": null,
- "Barneveld Centrum": null,
- "Barneveld Noord": null,
- "Barneveld Zuid": null,
- "Barnstorf(Han)": null,
- "Barnten": null,
- "Barrien": null,
- "Barsinghausen": null,
- "Bartenheim(Bale)": null,
- "Barth": null,
- "Barthmühle": null,
- "Baruth(Mark)": null,
- "Bascharage-Sanem": null,
- "Basdahl Kluste": null,
- "Basdorf": null,
- "Basel Bad Bf": null,
- "Basel Dreispitz": null,
- "Basel SBB": null,
- "Basel St Johann": null,
- "Bassersdorf": null,
- "Bassum": null,
- "Batzenhäusle": null,
- "Batzhausen": null,
- "Bauerbach": null,
- "Baumholder": null,
- "Baunach": null,
- "Baunatal-Guntershausen": null,
- "Baunatal-Rengershausen": null,
- "Baunhoej": null,
- "Bautzen": null,
- "Bavendorf": null,
- "Bayerbach": null,
- "Bayerisch Eisenstein": null,
- "Bayerisch Gmain": null,
- "Bayonne": null,
- "Bayreuth Hbf": null,
- "Bayreuth-St Georgen": null,
- "Bayrischzell": null,
- "Bebitz": null,
- "Bebra": null,
- "Bechstedt-Trippstein": null,
- "Beckingen(Saar)": null,
- "Beckum-Neubeckum": null,
- "Bedburg(Erft)": null,
- "Bedburg-Hau": null,
- "Bedum": null,
- "Beek-Elsloo": null,
- "Beelen": null,
- "Beelitz Stadt": null,
- "Beelitz-Heilstätten": null,
- "Beerfelden Hetzbach": null,
- "Beernem": null,
- "Beesd": null,
- "Beeskow": null,
- "Beetz-Sommerfeld": null,
- "Behringersdorf": null,
- "Beienheim": null,
- "Beilen": null,
- "Beilrode": null,
- "Beimerstetten": null,
- "Bekescsaba": null,
- "Bela pod Bezdezem": null,
- "Beldorf": null,
- "Belfort Ville": null,
- "Belgershain": null,
- "Belleben": null,
- "Bellegarde(Ain)": null,
- "Bellenberg": null,
- "Belleville Meurthe et Moselle": null,
- "Bellheim Am Mühlbuckel": null,
- "Bellheim Bf": null,
- "Bellinzona": null,
- "Belp": null,
- "Belval Lycée": null,
- "Belval-Rédange": null,
- "Belval-Université": null,
- "Belvaux-Soleuvre": null,
- "Bempflingen": null,
- "Benediktbeuern": null,
- "Benesov n. Ploucnici": null,
- "Benestroff": null,
- "Benfeld(Selestat)": null,
- "Bengel": null,
- "Bening": 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,
- "Berchem(LUX)": null,
- "Berchtesgaden Hbf": null,
- "Berg(CH)": null,
- "Berg(Pfalz)": null,
- "Berga(Elster)": null,
- "Berga-Kelbra": null,
- "Bergen auf Rügen": null,
- "Bergen op Zoom": null,
- "Bergen(Oberbay)": null,
- "Bergenweiler": null,
- "Bergfelde(b Berlin)": null,
- "Berghausen Am Stadion": null,
- "Berghausen Hummelberg": null,
- "Berghausen Pfinzbrücke": null,
- "Berghausen(Baden)": null,
- "Berghausen(Pfalz)": null,
- "Berghausen(b Wittgenstein)": null,
- "Bergheim(Erft)": null,
- "Bergheim-Giflitz": null,
- "Bergisch Gladbach": null,
- "Bergsdorf": null,
- "Bergtheim": null,
- "Bergues(Coudek)": null,
- "Bergwitz": null,
- "Bergün/Bravuogn": null,
- "Beringen Bad Bf": null,
- "Beringerfeld": null,
- "Beringhausen": null,
- "Beringstedt": null,
- "Berka(Wipper)": 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 Beusselstraße": null,
- "Berlin Bornholmer Str.": null,
- "Berlin Botanischer Garten": null,
- "Berlin Brandenburger Tor": null,
- "Berlin Buckower Chaussee": null,
- "Berlin Bundesplatz": null,
- "Berlin Charlottenburg (S)": null,
- "Berlin Eichborndamm": null,
- "Berlin Feuerbachstr.": null,
- "Berlin Frankfurter Allee": null,
- "Berlin Friedrichstraße": null,
- "Berlin Friedrichstraße (S)": null,
- "Berlin Gehrenseestraße": 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 Ostbahnhof (S)": null,
- "Berlin Ostkreuz": 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 Treptower Park": null,
- "Berlin Wannsee": null,
- "Berlin Wannsee (S)": 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-Hohenschönhausen (S)": null,
- "Berlin-Johannisthal": null,
- "Berlin-Karlshorst": null,
- "Berlin-Karlshorst (S)": null,
- "Berlin-Karow": null,
- "Berlin-Kaulsdorf": null,
- "Berlin-Köpenick": null,
- "Berlin-Lankwitz": null,
- "Berlin-Lichtenberg": null,
- "Berlin-Lichtenberg (S)": 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-Mahlsdorf (S)": null,
- "Berlin-Marienfelde": null,
- "Berlin-Marzahn": null,
- "Berlin-Neukölln": null,
- "Berlin-Nikolassee": null,
- "Berlin-Oberspree": null,
- "Berlin-Pankow": null,
- "Berlin-Pankow-Heinersdorf": null,
- "Berlin-Pichelsberg": null,
- "Berlin-Rahnsdorf": null,
- "Berlin-Rummelsburg": null,
- "Berlin-Schlachtensee": null,
- "Berlin-Schulzendorf": null,
- "Berlin-Schöneberg": null,
- "Berlin-Schöneweide": null,
- "Berlin-Schöneweide (S)": null,
- "Berlin-Schönholz": null,
- "Berlin-Spandau": null,
- "Berlin-Spandau (S)": 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,
- "Berlingen URh": null,
- "Berlingen(CH)": null,
- "Bermatingen-Ahausen": null,
- "Bern": null,
- "Bernau (S)": null,
- "Bernau a Chiemsee": null,
- "Bernau(b Berlin)": null,
- "Bernau-Friedenstal": null,
- "Bernay": null,
- "Bernburg Hbf": null,
- "Bernburg-Friedenshall": null,
- "Bernburg-Roschwitz": null,
- "Bernburg-Strenzfeld": null,
- "Bernburg-Waldau": null,
- "Berne": null,
- "Bernried": null,
- "Bernterode": null,
- "Beroun": null,
- "Bersenbrück": null,
- "Berthelming": null,
- "Berthelsdorf(Erzgeb)": null,
- "Berthelsdorf(Erzgebirge) Ort": null,
- "Bertrange-Strassen": null,
- "Bertrix": null,
- "Bertsdorf": null,
- "Berzhahn": null,
- "Besançon-Mouillère": null,
- "Besançon-Viotte": null,
- "Besch": null,
- "Besigheim": null,
- "Besseringen": null,
- "Best": null,
- "Bestensee": null,
- "Bestwig": null,
- "Bettembourg": null,
- "Bettembourg(fr)": null,
- "Bettmannsäge": null,
- "Bettwiesen": null,
- "Betzdorf(LUX)": null,
- "Betzdorf(Sieg)": null,
- "Beucha": null,
- "Beuchow": null,
- "Beuggen": null,
- "Beuna(Geiseltal)": null,
- "Beuren": null,
- "Beuron": null,
- "Beutelsbach": null,
- "Beutersitz": null,
- "Beverungen-Wehrden": null,
- "Beverwijk": null,
- "Bex": null,
- "Bexbach": null,
- "Beyendorf": null,
- "Beziers": null,
- "Biarritz": null,
- "Biasca": null,
- "Bibelöd": null,
- "Biberach(Baden)": null,
- "Biberach(Riß)": null,
- "Biberach(Riß) Süd": null,
- "Biberist Ost": null,
- "Biberist RBS": null,
- "Biblis": null,
- "Bibra": null,
- "Bichl": null,
- "Bichlbach Almkopfbahn": null,
- "Bichlbach-Berwang": null,
- "Bickenbach(Bergstr)": null,
- "Biebesheim": null,
- "Biedenkopf": null,
- "Biedenkopf Campus": null,
- "Biederitz": null,
- "Biel/Bienne": null,
- "Bielefeld Hbf": null,
- "Bielefeld Ost": null,
- "Bielefeld-Brackwede": null,
- "Bielefeld-Senne": null,
- "Bielefeld-Sennestadt": null,
- "Bielefeld-Windelsbleiche": null,
- "Biendorf": null,
- "Bienenbüttel": null,
- "Bienenmühle": null,
- "Bierbach": null,
- "Bieren-Rödinghausen": null,
- "Bieringen": null,
- "Biersdorf(Westerw)": null,
- "Biersdorf-Ort(Ww)": null,
- "Bierset-Awans": null,
- "Biesenrode": null,
- "Biesenthal": null,
- "Biessenhofen": null,
- "Bietigheim(Baden)": null,
- "Bietigheim-Bissingen": null,
- "Bietingen": null,
- "Bigge": null,
- "Bildstock": null,
- "Bilfingen": null,
- "Bilina": null,
- "Billenhausen": null,
- "Billerbeck": null,
- "Billum st": null,
- "Bilten": null,
- "Bilthoven": null,
- "Bily Kostel nad Nisou": null,
- "Binau": null,
- "Bindfelde": null,
- "Bindlach": null,
- "Bingen(Rhein) Hbf": null,
- "Bingen(Rhein) Stadt": null,
- "Bingen-Gaulsheim": null,
- "Binolen": 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,
- "Birmensdorf ZH": null,
- "Birresborn": null,
- "Bischheim-Gersdorf": null,
- "Bischofshofen": 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,
- "Blainville-Damelevieres": null,
- "Blaj": null,
- "Blankenbach": null,
- "Blankenberg(Meckl)": null,
- "Blankenberg(Sieg)": null,
- "Blankenburg(Harz)": null,
- "Blankenfelde (S)": null,
- "Blankenfelde(Teltow-Fläming)": null,
- "Blankenheim(Sangerhausen)": null,
- "Blankenheim(Wald)": null,
- "Blankenloch": null,
- "Blankenloch Kirche, Stutensee": null,
- "Blankenloch Mühlenweg, Stutensee": null,
- "Blankenloch Nord, Stutensee": null,
- "Blankenloch Süd, Stutensee": null,
- "Blankenloch Tolna-Platz, Stutensee": null,
- "Blankensee(Meckl)": null,
- "Blankenstein(Saale)": null,
- "Blaubeuren": null,
- "Blaufelden": null,
- "Blausee-Mitholz": null,
- "Blaustein": null,
- "Blechhammer(Thür)": null,
- "Bleibach": null,
- "Bleichenbach(Oberh)": null,
- "Bleicherode Ost": null,
- "Blens": null,
- "Blerick": null,
- "Blieskastel-Lautzkirchen": null,
- "Blindenmarkt": null,
- "Blindheim": null,
- "Bloemendaal": null,
- "Bludenz": null,
- "Bludenz Brunnenfeld": null,
- "Bludenz-Moos": null,
- "Blumberg(b Berlin)": null,
- "Blumberg-Rehhahn": null,
- "Blumberg-Riedöschingen": null,
- "Blumberg-Zollhaus": null,
- "Blumenau": null,
- "Blumenhagen": null,
- "Blumenthal(Mark)": null,
- "Blönsdorf": null,
- "Bobenheim": null,
- "Bobingen": null,
- "Bobitz": null,
- "Bobstadt": null,
- "Bobzin": null,
- "Bocholt": null,
- "Bochum Hbf": null,
- "Bochum West": null,
- "Bochum-Dahlhausen": null,
- "Bochum-Ehrenfeld": null,
- "Bochum-Hamme": null,
- "Bochum-Langendreer": null,
- "Bochum-Langendreer West": null,
- "Bochum-Riemke": null,
- "Bockenheim-Kindenheim": null,
- "Bodegraven": null,
- "Bodelsberg": null,
- "Bodelshausen": null,
- "Bodenburg": null,
- "Bodenfelde": null,
- "Bodenheim": null,
- "Bodenmais": null,
- "Bodenrode": null,
- "Bodenwöhr Nord": null,
- "Bodio TI": null,
- "Boen(F)": null,
- "Bogen": null,
- "Bohmte": null,
- "Bohumin": null,
- "Boisheim": null,
- "Boizenburg(Elbe)": null,
- "Bokholt": null,
- "Bollwiller(Lutterb)": null,
- "Bologna Centrale": null,
- "Bolzano/Bozen": null,
- "Bondorf(b Herrenberg)": null,
- "Bonn Brühler Str.": 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 UN Campus": 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,
- "Boondael/Boondaal": 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,
- "Bordeaux-St-Jean": null,
- "Bordesholm": null,
- "Borgeln": null,
- "Borgholzhausen": null,
- "Borgo S. Dalmazzo": null,
- "Borgsdorf": null,
- "Bork(Westf)": null,
- "Borken(Hess)": null,
- "Borken(Westf)": null,
- "Borkheide": null,
- "Borna(Leipzig)": null,
- "Borne(NL)": null,
- "Bornholte(b Verl)": null,
- "Borsdorf(Hess)": null,
- "Borsdorf(Sachs)": null,
- "Boskoop": null,
- "Boskoop Snijdelwijk": null,
- "Bottighofen": null,
- "Bottrop Hbf": null,
- "Bottrop-Boy": null,
- "Bottrop-Vonderort": null,
- "Boulevarden st": null,
- "Bourg-St.Maurice": null,
- "Bourg-en-Bresse": null,
- "Bourges": null,
- "Bous(Saar)": null,
- "Boven-Hardinxveld": null,
- "Bovenkarspel Flora": null,
- "Bovenkarspel-Grootebroek": null,
- "Boxberg-Wölchingen": null,
- "Boxmeer": null,
- "Boxtel": null,
- "Brachbach": null,
- "Brachelen": null,
- "Brahlstorf": null,
- "Brake(Unterweser)": null,
- "Brake(b Bielefeld)": null,
- "Brakel(Höxter)": null,
- "Bramming st": null,
- "Bramsche": null,
- "Bramstedt(b Syke)": null,
- "Brand Tropical Islands": null,
- "Brandenburg Altstadt": null,
- "Brandenburg Hbf": null,
- "Brandoberndorf": null,
- "Brandstätt": null,
- "Brannenburg": null,
- "Brasov": null,
- "Bratislava hl.st.": null,
- "Bratislava-Petrzalka": null,
- "Braubach": null,
- "Braunau/Inn": null,
- "Braunsbedra": null,
- "Braunsbedra Ost": null,
- "Braunschweig Hbf": null,
- "Braunschweig-Gliesmarode": null,
- "Braunsdorf-Lichtenwalde": null,
- "Brebach": null,
- "Breclav": null,
- "Breclav(Gr)": null,
- "Breda": null,
- "Breda-Prinsenbeek": null,
- "Breddin": null,
- "Bredebro st": null,
- "Bredelar": null,
- "Bredenbek": null,
- "Bredstedt": null,
- "Brefeld": null,
- "Bregenz": null,
- "Bregenz Hafen": null,
- "Bregenz Riedenburg": null,
- "Brehna": null,
- "Breil-sur-Roya": 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,
- "Brennero/Brenner": null,
- "Brescia": null,
- "Bressanone/Brixen": null,
- "Bressoux": null,
- "Brest(F)": 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,
- "Breukelen": null,
- "Breyell": null,
- "Breziny u Decina": null,
- "Brieselang": null,
- "Briesen(Mark)": null,
- "Brig": null,
- "Brigachtal Kirchdorf": null,
- "Brigachtal Klengen": null,
- "Brilon Stadt": null,
- "Brilon Wald": null,
- "Britz": null,
- "Brixen im Thale": null,
- "Brixlegg": null,
- "Brno hl.n.": null,
- "Brocken": null,
- "Brockhöfe": null,
- "Broderstorf": null,
- "Broens st": null,
- "Brohl": null,
- "Brokstedt": null,
- "Bronschhofen": 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 Tunnelstraße": null,
- "Bruchweiler": null,
- "Bruck-Fusch": null,
- "Bruck/Leitha": null,
- "Bruck/Mur": null,
- "Bruckberg": null,
- "Brucken": null,
- "Bruckmühl": null,
- "Brugg AG": null,
- "Brugge": null,
- "Brumath": null,
- "Brummen": null,
- "Brunau-Packebusch": null,
- "Brunico/Bruneck": null,
- "Brunnen(CH)": null,
- "Brunnen(Oberbay)": null,
- "Brussels Airport - Zaventem": null,
- "Bruxelles Midi": null,
- "Bruxelles-Central": null,
- "Bruxelles-Luxembourg": null,
- "Bruxelles-Midi Eurostar": null,
- "Bruxelles-Nord": 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ü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,
- "Buchs SG": null,
- "Buckow(Beeskow)": null,
- "Bucuresti Nord Gara A": null,
- "Budapest-Ferencváros": null,
- "Budapest-Keleti": null,
- "Budapest-Nyugati": null,
- "Buddenhagen": null,
- "Budenheim": null,
- "Bufleben": null,
- "Buggingen": null,
- "Buir": null,
- "Buitenpost": null,
- "Buldern": null,
- "Bullay(DB)": null,
- "Bully-Grenay": null,
- "Bunde": null,
- "Bundenthal-Rumbach": null,
- "Bunnik": 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,
- "Burgfried b.Gnas": 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,
- "Buschmühle": null,
- "Buschow": null,
- "Busenbach": null,
- "Busenberg-Schindhard": null,
- "Busigny": null,
- "Bussnang": null,
- "Bussum Zuid": null,
- "Busto Arsizio": null,
- "Buttenheim": null,
- "Buttstädt": null,
- "Butzbach": null,
- "Buxtehude": null,
- "Bydgoszcz Glowna": null,
- "Bäch": 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öckstein": null,
- "Bödigheim": null,
- "Böheimkirchen": 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önen-Nordbögge": null,
- "Bönningstedt": null,
- "Börnecke(Harz)": null,
- "Börßum": null,
- "Bösdorf(Sachs-Anh)": null,
- "Bösperde": null,
- "Bötzingen": null,
- "Bötzingen Mühle": null,
- "Bübingen": null,
- "Büchen": null,
- "Büchenbach": null,
- "Büches-Düdelsheim": null,
- "Büchig, Stutensee": null,
- "Bückeburg": null,
- "Büdingen(Oberhess)": null,
- "Büdingen(Westerw)": null,
- "Bühl(Baden)": null,
- "Bülach": null,
- "Bülzig": null,
- "Bünde(Westf)": null,
- "Bürgeln": null,
- "Bürgerhaus, Hessisch Lichtenau": null,
- "Bürglen": null,
- "Bürstadt": null,
- "Büsenbachtal": null,
- "Büsum": null,
- "Büttgen": null,
- "Bützow": null,
- "Cadenazzo": null,
- "Cadenberge": null,
- "Cadolzburg": null,
- "Cainsdorf": null,
- "Calais Ville": null,
- "Calais-Fréthun": null,
- "Calau(Nl)": null,
- "Calbe(Saale) Ost": null,
- "Calbe(Saale) Stadt": null,
- "Calbe(Saale) West": null,
- "Calberlah": null,
- "Caldern": null,
- "Caldes de Malavella": null,
- "Calmbach Bahnhof": null,
- "Calmbach Süd": null,
- "Calw": null,
- "Camburg(Saale)": null,
- "Cammin(Meckl)": null,
- "Campo di Trens/Freienfeld": null,
- "Cannes": null,
- "Cannes-la-Bocca": null,
- "Capelle Schollevaar": null,
- "Capelle(Westf)": null,
- "Capellen": null,
- "Capolago-Riva S. Vitale": null,
- "Caputh Schwielowsee": null,
- "Caputh-Geltow": null,
- "Carbonne(Boussens)": null,
- "Carcassonne": null,
- "Carimate": null,
- "Carnoules(Toulon)": null,
- "Casekow": null,
- "Casteldarne/Ehrenburg": null,
- "Castelnaudary": null,
- "Castione-Arbedo": null,
- "Castricum": null,
- "Castrop-Rauxel Hbf": null,
- "Castrop-Rauxel Süd": null,
- "Castrop-Rauxel-Merklinde": null,
- "Cavaillon(Avignon)": null,
- "Celle": null,
- "Centallo": null,
- "Cents-Hamm": null,
- "Cerbère": null,
- "Ceska Kamenice": null,
- "Ceska Kubice": null,
- "Ceska Lipa hl.n.": null,
- "Ceska Lipa strelnice": null,
- "Ceske Budejovice": null,
- "Ceske Velenice": null,
- "Chalon sur Saône": null,
- "Chalons en Champagne": null,
- "Cham(Oberpf)": null,
- "Chambery-Challes-E": null,
- "Chamerau": null,
- "Champigneulles": null,
- "Charleroi Sud": null,
- "Chateau-Thierry": null,
- "Chauny(Tergnier)": null,
- "Cheb": null,
- "Cheb-Skalka": null,
- "Chelles Gournay": null,
- "Chemnitz Alt Chemnitz Center": null,
- "Chemnitz Annenstraße": null,
- "Chemnitz Bernsbachplatz": 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 Küchwald": null,
- "Chemnitz Mitte": null,
- "Chemnitz Moritzhof": null,
- "Chemnitz Omnibusbahnhof": null,
- "Chemnitz Riemenschneiderstraße": null,
- "Chemnitz Rosenbergstraß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 Stadlerplatz": null,
- "Chemnitz Süd": null,
- "Chemnitz TU Campus": null,
- "Chemnitz Technopark": null,
- "Chemnitz Theaterplatz": null,
- "Chemnitz Treffurthstraße": null,
- "Chemnitz Uhlestraße": null,
- "Chemnitz Zentralhaltestelle": null,
- "Chemnitz-Altchemnitz": null,
- "Chemnitz-Borna Hp": null,
- "Chemnitz-Harthau": null,
- "Chemnitz-Hilbersdorf": null,
- "Chemnitz-Reichenhain": null,
- "Chemnitz-Schönau": null,
- "Chemnitz-Siegmar": null,
- "Chenay Gagny": null,
- "Chenee": null,
- "Cherbourg": null,
- "Chevremont(NL)": null,
- "Chiasso": null,
- "Chiusa/Klausen": null,
- "Chomutov": null,
- "Chomutov mesto": null,
- "Chorin": null,
- "Chotyne": null,
- "Chrastava": null,
- "Chrastava-Andelska Hora": null,
- "Chribska": null,
- "Chur": null,
- "Château du Loir": null,
- "Château-Arnoux-St-Auban": null,
- "Châteauroux": null,
- "Châtelet": null,
- "Cintegabelle": null,
- "Clarholz": null,
- "Clausnitz": null,
- "Clermont-Ferrand": null,
- "Clerval": null,
- "Clervaux": null,
- "Cloppenburg": null,
- "Coburg": null,
- "Coburg Nord": null,
- "Coburg-Beiersdorf": null,
- "Coburg-Neuses": null,
- "Cochem(Mosel)": null,
- "Coesfeld Schulzentrum": null,
- "Coesfeld(Westf)": null,
- "Coevorden": null,
- "Colle Isarco/Gossensass": null,
- "Collenberg": null,
- "Collioure": null,
- "Colmar": null,
- "Combs la Ville Quincy": null,
- "Como S. Giovanni": null,
- "Compiegne": null,
- "Conegliano": null,
- "Conflans-Jarny": null,
- "Contwig": null,
- "Coppenbrügge": null,
- "Corbehem(Douai)": null,
- "Corbeil Essonnes": null,
- "Cornaux": null,
- "Coschen": null,
- "Cosne": null,
- "Cossebaude": null,
- "Cossonay-Penthalaz": null,
- "Coswig(Anh)": null,
- "Coswig(b Dresden)": null,
- "Cottbus Hbf": null,
- "Cottbus-Merzdorf": null,
- "Cottbus-Sandow": null,
- "Cottbus-Willmersdorf Nord": null,
- "Coulommiers": null,
- "Courcelles-sur-Nied": null,
- "Coutras": null,
- "Crailsheim": null,
- "Cranzahl": null,
- "Creidlitz": null,
- "Creil": null,
- "Creußen(Oberfr)": null,
- "Crimmitschau": null,
- "Crivitz": null,
- "Crossen Ort": null,
- "Crossen a d Elster": null,
- "Cuijk": null,
- "Culemborg": null,
- "Culmont-Chalindrey": null,
- "Culoz": null,
- "Cuneo": null,
- "Cunnertswalde": null,
- "Cursdorf": null,
- "Curtici": null,
- "Cuxhaven": null,
- "Czerwiensk": null,
- "Cölbe": null,
- "Daaden": null,
- "Daarlerveen": null,
- "Dabendorf": null,
- "Dachau Bahnhof": null,
- "Dachau Stadt": null,
- "Dachrieden": null,
- "Dachsen": null,
- "Dachwig": null,
- "Dagebüll Kirche": null,
- "Dagebüll Mole": null,
- "Dagmersellen": null,
- "Dahl": null,
- "Dahlbruch": null,
- "Dahlem(Eifel)": null,
- "Dahlen(Sachs)": null,
- "Dahlenburg": null,
- "Dahlerbrück": null,
- "Dahlewitz": null,
- "Dahn": null,
- "Dahn Süd": null,
- "Dalen(NL)": null,
- "Dalfsen": null,
- "Dalheim": null,
- "Dallau": null,
- "Dallgow-Döberitz": null,
- "Dambeck(Altm)": null,
- "Dammerstock, Karlsruhe": 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,
- "Davos Dorf": null,
- "Davos Platz": null,
- "Dax": null,
- "Daxlanden Dornröschenweg, Karlsruhe": null,
- "Daxlanden Karl-Delisle-Straße, Karlsruhe": null,
- "Daxlanden Nussbaumweg, Karlsruhe": null,
- "Daxlanden Thomas-Mann-Straße, Karlsruhe": null,
- "De Vink": null,
- "Debrecen": null,
- "Decin hl.n.": null,
- "Decin vychod": null,
- "Decin-Certova voda": null,
- "Decin-Priper": null,
- "Decin-Prostredni Zleb": null,
- "Dedenhausen": null,
- "Dedensen-Gümmer": null,
- "Dedinghausen": null,
- "Deezbüll": null,
- "Deggendorf Hbf": null,
- "Deidesheim": null,
- "Deining(Oberpf)": null,
- "Deinste": null,
- "Deinum": null,
- "Deisenhofen": null,
- "Deißlingen Mitte": null,
- "Delden": null,
- "Delft": null,
- "Delft Campus": null,
- "Delfzijl": null,
- "Delfzijl West": null,
- "Delitzsch ob Bf": null,
- "Delitzsch unt Bf": null,
- "Dellfeld": null,
- "Dellfeld Ort": null,
- "Delmenhorst": null,
- "Delmenhorst Hasporter Damm": null,
- "Delémont": null,
- "Demitz-Thumitz": null,
- "Demker": null,
- "Demmin": null,
- "Den Dolder": null,
- "Den Haag Centraal": null,
- "Den Haag HS": null,
- "Den Haag Laan van Nieuw Oost Indie": null,
- "Den Haag Mariahoeve": null,
- "Den Haag Moerwijk": null,
- "Den Haag Ypenburg": null,
- "Den Helder": null,
- "Den Helder Zuid": null,
- "Denderleeuw": null,
- "Densborn": null,
- "Denzlingen": null,
- "Dernau": null,
- "Dernbach(Westerw)": null,
- "Derneburg(Han)": null,
- "Desenice": null,
- "Desenzano del Garda/Sirmione": null,
- "Desio": 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,
- "Dettwiller": null,
- "Deuben(Zeitz)": null,
- "Deuerling": null,
- "Deurne": null,
- "Deuten": null,
- "Deutzen": null,
- "Deva": null,
- "Deventer": null,
- "Deventer Colmschate": null,
- "Devinska Nova Ves": null,
- "Didam": null,
- "Diebach": null,
- "Dieburg": null,
- "Diedelsheim": null,
- "Diedorf(Schwab)": null,
- "Diemen": null,
- "Diemen Zuid": null,
- "Diemeringen": null,
- "Dienheim": null,
- "Diepenbeek": null,
- "Diepholz": null,
- "Dieren": null,
- "Dieskau": null,
- "Diessenhofen": null,
- "Diessenhofen URh": null,
- "Dietersheim": null,
- "Dietlikon": null,
- "Dietmannsried": null,
- "Dietzelbach": null,
- "Dietzenbach Bahnhof": null,
- "Dietzenbach Mitte": null,
- "Dietzenbach-Steinberg": null,
- "Dietzhausen": null,
- "Dieulouard": null,
- "Diez": null,
- "Diez Ost": null,
- "Dießen": null,
- "Differdange": null,
- "Dijon Porte Neuve": null,
- "Dijon Ville": null,
- "Dillbrecht": null,
- "Dillenburg": null,
- "Dillingen(Donau)": null,
- "Dillingen(Saar)": null,
- "Dingolfing": null,
- "Dinkelsbühl Bf": null,
- "Dinkelscherben": null,
- "Dinslaken": null,
- "Dippach-Reckange": null,
- "Dippoldiswalde": null,
- "Dirmingen": null,
- "Dissen-Bad Rothenfelde": null,
- "Distelhausen": null,
- "Ditfurt": null,
- "Dittersbach": null,
- "Dittersdorf": null,
- "Dittigheim": null,
- "Ditzingen": null,
- "Dobbiaco/Toblach": null,
- "Doberlug-Kirchhain": null,
- "Doberschütz": null,
- "Dobova": null,
- "Dobova(Gr)": null,
- "Dodendorf": null,
- "Dodenhof": null,
- "Doestrup(Soenderjylland) st": null,
- "Doetinchem": null,
- "Doetinchem De Huet": null,
- "Dogern": null,
- "Dohna(Sachs)": null,
- "Doksy": null,
- "Dole Ville": null,
- "Dolhain-Gileppe": null,
- "Dollbergen": null,
- "Dollern": null,
- "Dollnstein": null,
- "Dolni Habartice": null,
- "Dolni Podluzi": null,
- "Dolni Poustevna": null,
- "Dolni Zleb": null,
- "Dolni Zleb zast.": null,
- "Domazlice": null,
- "Dombühl": null,
- "Dommeldange": null,
- "Domnitz(Saalkr)": null,
- "Domodossola": null,
- "Domsühl": null,
- "Donaueschingen": null,
- "Donaueschingen Allmendshofen": null,
- "Donaueschingen Aufen": null,
- "Donaueschingen Grüningen": null,
- "Donaueschingen Mitte/Siedlung": null,
- "Donauwörth": null,
- "Dordrecht": null,
- "Dordrecht Stadspolders": null,
- "Dordrecht Zuid": null,
- "Dorf Mecklenburg": null,
- "Dorfchemnitz": null,
- "Dorfen Bahnhof": null,
- "Dorfgastein": null,
- "Dorfmark": null,
- "Dorfprozelten": null,
- "Dorheim(Wetterau)": null,
- "Dormagen": null,
- "Dormagen Chempark": null,
- "Dornbirn": null,
- "Dornbirn Schoren": null,
- "Dornburg(Saale)": null,
- "Dornstetten": null,
- "Dornstetten-Aach": null,
- "Dorsten": null,
- "Dortelweil": 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 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,
- "Dottikon-Dintikon": null,
- "Dourges": null,
- "Drahnsdorf": null,
- "Drahtzug": null,
- "Drauffelt": null,
- "Drebkau": null,
- "Drei Annen Hohne": 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,
- "Driebergen-Zeist": null,
- "Driehuis": null,
- "Drohndorf-Mehringen": null,
- "Dronryp": null,
- "Dronten": null,
- "Duchcov": null,
- "Ducherow": null,
- "Duckterath": null,
- "Dudweiler": null,
- "Dugo Selo": null,
- "Duisburg Entenfang": null,
- "Duisburg Hbf": null,
- "Duisburg-Buchholz": null,
- "Duisburg-Großenbaum": 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,
- "Duiven": null,
- "Duivendrecht": null,
- "Dunkerque": null,
- "Durach": null,
- "Durlach Hubstraße, Karlsruhe": null,
- "Durlach Untermühlstraße, Karlsruhe": null,
- "Durlacher Tor/KIT-Campus Süd, Karlsruhe": null,
- "Durmersheim": null,
- "Durmersheim Nord": null,
- "Dutenhofen(Wetzlar)": null,
- "Dußlingen": null,
- "Dyreby st": null,
- "Däniken": 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öttingen": null,
- "Dülken": null,
- "Dülmen": 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ürrnhaar": null,
- "Dürrröhrsdorf": 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-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,
- "Eben im Pongau": null,
- "Ebenfurth": 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,
- "Ebersheim": null,
- "Eberswalde Hbf": null,
- "Ebertsheim": null,
- "Ebing": null,
- "Ebringen": null,
- "Ebstorf(Uelzen)": null,
- "Echem": null,
- "Eching": null,
- "Echt": null,
- "Echterdingen": null,
- "Echzell": null,
- "Eckardtsleben": null,
- "Eckartsberga(Thür)": null,
- "Eckartshausen-Ilshofen": null,
- "Eckenerstraße, Karlsruhe": null,
- "Eckernförde": null,
- "Eckersmühlen": null,
- "Eddersheim": null,
- "Ede Centrum": null,
- "Ede(B)": null,
- "Ede-Wageningen": null,
- "Edelfingen": null,
- "Edenkoben": null,
- "Edermünde-Grifte": null,
- "Edesheim(Pfalz)": null,
- "Ediger-Eller": null,
- "Edingen(Wetzlar)": null,
- "Edle Krone": null,
- "Edling": null,
- "Eemshaven": null,
- "Effelder(Thür)": null,
- "Effolderbach": null,
- "Effretikon": null,
- "Efringen-Kirchen": null,
- "Egelsbach": null,
- "Egersdorf": null,
- "Egestorf(Deister)": null,
- "Eggenfelden": null,
- "Eggenfelden Mitte": null,
- "Eggenstein Bf": null,
- "Eggenstein Schweriner Straße, Eggenstein-Leopoldsh": null,
- "Eggenstein Spöcker Weg, Eggenstein-Leopoldshafen": null,
- "Eggenstein Süd, Eggenstein-Leopoldshafen": null,
- "Eggersdorf": null,
- "Eggesin": null,
- "Eggingen": null,
- "Egglkofen": null,
- "Eggmühl": null,
- "Eggolsheim": null,
- "Eglharting": null,
- "Egling": null,
- "Eglisau": null,
- "Egnach": 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,
- "Ehrwald Zugspitzbahn": null,
- "Eibau": null,
- "Eich(Sachs)": 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,
- "Eijsden": null,
- "Eilenburg": null,
- "Eilenburg Ost": null,
- "Eilendorf": null,
- "Eilsleben(b Magdeburg)": null,
- "Eilvese": null,
- "Eimeldingen": null,
- "Einbeck Mitte": null,
- "Einbeck Otto-Hahn-Straße": null,
- "Einbeck-Salzderhelden": null,
- "Eindhoven Centraal": null,
- "Eindhoven Strijp-S": null,
- "Einfeld": null,
- "Einsiedel": null,
- "Einsiedel Brauerei": null,
- "Einsiedel Hp Gymnasium": null,
- "Einsiedeln": null,
- "Einsiedlerhof": null,
- "Einöd(Saar)": null,
- "Eisemroth": null,
- "Eisenach": null,
- "Eisenach Opelwerke Hp": null,
- "Eisenach West": 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,
- "Elne": null,
- "Elpersheim": null,
- "Elsbethen": null,
- "Elsfleth": null,
- "Elsholz": null,
- "Elsnigk(Anh)": null,
- "Elst": 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,
- "Emmen Zuid": null,
- "Emmen(NL)": null,
- "Emmenbrücke": null,
- "Emmendingen": null,
- "Emmerich": null,
- "Emmerich-Elten": 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,
- "Engis": null,
- "Engstingen": null,
- "Engstingen Schulzentrum": null,
- "Engstlatt": null,
- "Enkenbach": null,
- "Enkhuizen": null,
- "Ennepetal": null,
- "Enns": null,
- "Enschede": null,
- "Enschede De Eschmarke": null,
- "Enschede Kennispark": null,
- "Ensdorf(Saar)": null,
- "Enspel": null,
- "Entenfang, Karlsruhe": null,
- "Entringen": null,
- "Enzberg": null,
- "Enzisweiler": null,
- "Epe(Westf)": null,
- "Epernay": null,
- "Epierre-St Leger": null,
- "Epinal": 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,
- "Erbprinz/Schloss, Ettlingen": 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,
- "Erkner (S)": 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,
- "Erlen": null,
- "Erlenbach(Main)": null,
- "Ermatingen": null,
- "Ermatingen URh": null,
- "Ermelo": null,
- "Erndtebrück": null,
- "Ernsgaden": null,
- "Ernsthausen": null,
- "Ernstthal am Rennsteig": null,
- "Erpel(Rhein)": null,
- "Erpolzheim": null,
- "Erquelinnes": null,
- "Ersingen": null,
- "Ersingen West": null,
- "Erstein": null,
- "Erstfeld": null,
- "Erzhausen": null,
- "Erzingen(Baden)": null,
- "Erzingen(Württ)": null,
- "Esbjerg st": null,
- "Esch-sur-Alzette": null,
- "Eschborn": null,
- "Eschborn Süd": null,
- "Eschede": null,
- "Eschelbronn": null,
- "Eschenau(Mittelfr)": null,
- "Eschenau(b Heilbronn)": null,
- "Eschenau/Salzach": null,
- "Eschenbach(b Markt Erlbach)": null,
- "Eschenlohe": null,
- "Escherndorf-Vogelsburg": 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,
- "Eslöv station": null,
- "Espelkamp": null,
- "Espenau-Mönchehof": null,
- "Essel": null,
- "Essen Hbf": null,
- "Essen Stadtwald": null,
- "Essen Süd": null,
- "Essen West": null,
- "Essen(B)": 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,
- "Essenweinstraße, Karlsruhe": null,
- "Esslingen(Neckar)": null,
- "Esslingen-Mettingen": null,
- "Esslingen-Zell": null,
- "Esting": null,
- "Etampes": null,
- "Etelsen": null,
- "Ettelbruck": null,
- "Etten-Leur": null,
- "Ettenhausen": null,
- "Etterzhausen": null,
- "Ettlingen Stadt": null,
- "Ettlingen West": null,
- "Etzbach": null,
- "Etzelwang": null,
- "Etzenbach": null,
- "Etzenricht": null,
- "Etzenrot": null,
- "Etzleben": null,
- "Etzwilen": null,
- "Eubigheim": null,
- "Euerdorf": null,
- "Eupen": null,
- "Europaplatz/Postgal. (Kaiser), Karlsruhe": null,
- "Euskirchen": null,
- "Euskirchen Zuckerfabrik": null,
- "Euskirchen-Großbüllesheim": null,
- "Euskirchen-Kreuzweingarten": null,
- "Euskirchen-Kuchenheim": null,
- "Euskirchen-Stotzheim": null,
- "Eutin": null,
- "Eutingen Nord": null,
- "Eutingen im Gäu": null,
- "Eutingen(Baden)": null,
- "Evreux Normandie": null,
- "Eyach": null,
- "Eygelshoven": null,
- "Eygelshoven Markt": null,
- "Eystrup": null,
- "Eßleben": null,
- "Faak am See": null,
- "Fachingen(Lahn)": null,
- "Fahrenkrug": null,
- "Fahrnau": null,
- "Faido": null,
- "Falkenau(Sachs)Hp": null,
- "Falkenau(Sachs)Süd": 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,
- "Faulquemont": null,
- "Faurndau": null,
- "Favoritepark": null,
- "Feanwalden": null,
- "Fegersheim Lipsheim": null,
- "Fehmarn-Burg": null,
- "Fehraltorf": null,
- "Fehring": null,
- "Feilitzsch": null,
- "Feldafing": null,
- "Feldbach/Raab": null,
- "Feldberg-Bärental": null,
- "Felde": null,
- "Feldhausen": null,
- "Feldkirch": null,
- "Feldkirch Amberg": null,
- "Feldkirchen in Kärnten": null,
- "Feldkirchen(b München)": null,
- "Feldolling": null,
- "Felixdorf": null,
- "Fellbach": null,
- "Felsberg-Altenbrunslar": null,
- "Felsberg-Gensungen": null,
- "Felsberg-Wolfershausen": null,
- "Ferch-Lienewitz": null,
- "Ferdinandshof": null,
- "Fermerswalde": null,
- "Ferndorf(Siegen)": null,
- "Ferrara": null,
- "Feucht": null,
- "Feucht Ost": null,
- "Feucht-Moosbach": null,
- "Feudingen": null,
- "Fichtenberg": null,
- "Fieberbrunn": null,
- "Figueres": null,
- "Filderstadt": null,
- "Filisur": null,
- "Filsen": null,
- "Finkenheerd": null,
- "Finkenkrug": null,
- "Finnentrop": null,
- "Finningerstraße": null,
- "Finsterwald": null,
- "Finsterwalde(Niederlausitz)": null,
- "Firenze S.M.N.": null,
- "Fischbach(Nürnberg)": null,
- "Fischbach-Camphausen": null,
- "Fischbach-Weierbach": null,
- "Fischbachau": null,
- "Fischen": null,
- "Fischhaus": null,
- "Fischhausen-Neuhaus": null,
- "Fischweier, Karlsbad": null,
- "Flamatt": null,
- "Flassa": null,
- "Flaurling": null,
- "Flechtingen": null,
- "Fleetmark": null,
- "Flehingen": null,
- "Flensburg": null,
- "Flensburg-Weiche": null,
- "Flers": null,
- "Flieden": null,
- "Flintbek": null,
- "Flintsbach": null,
- "Flomersheim": null,
- "Floßmühle": null,
- "Flughafen BER - Terminal 1-2": null,
- "Flughafen BER - Terminal 1-2 (S-Bahn)": null,
- "Flughafen BER - Terminal 5 (Schönefeld)": null,
- "Flughafen Wien": null,
- "Flums": null,
- "Flöha": null,
- "Flöha-Plaue": null,
- "Flörsheim(Main)": null,
- "Flüelen": null,
- "Fohrde": null,
- "Fontaine": null,
- "Fontainebleau-Avon": null,
- "Fontan Saorge": null,
- "Forbach(F)": null,
- "Forbach(Schwarzw)": null,
- "Forchheim Hallenbad, Rheinstetten": null,
- "Forchheim Hauptstraße, Rheinstetten": null,
- "Forchheim Leichtsandstr./Messe Karlsruhe, Rheinste": null,
- "Forchheim Leichtsandstraße/Messe Karlsruhe, Rheins": null,
- "Forchheim Oberfeldstraße, Rheinstetten": null,
- "Forchheim(Oberfr)": null,
- "Forchheim(b Karlsruhe)": null,
- "Forest Midi/Vorst Zuid": null,
- "Fornsbach": null,
- "Forst(Lausitz)": null,
- "Forstfeldstraße, Kassel": null,
- "Forsthaus": null,
- "Forsting": null,
- "Fortezza/Franzensfeste": null,
- "Forth": null,
- "Fossano": null,
- "Fourchambault": null,
- "Frahelsbruck": null,
- "Fraipont": null,
- "Franeker": null,
- "Frankenberg(Eder)": null,
- "Frankenberg(Sachs)": null,
- "Frankenberg(Sachs) Süd": null,
- "Frankenberg-Goßberg": null,
- "Frankenberg-Viermünden": null,
- "Frankenmarkt": 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) Stresemannallee/Mörfelder Landstr": null,
- "Frankfurt(Main)-Gateway Gardens": 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-Mainkur": null,
- "Frankfurt-Nied": null,
- "Frankfurt-Niederrad": null,
- "Frankfurt-Rödelheim": null,
- "Frankfurt-Sindlingen": null,
- "Frankfurt-Sossenheim": null,
- "Frankfurt-Unterliederbach": null,
- "Frankfurt-Zeilsheim": null,
- "Frankleben": null,
- "Frantiskovy Lazne": null,
- "Frantiskovy Lazne Aquaforum": null,
- "Frastanz": null,
- "Frauenalb-Schielberg": null,
- "Frauenau": null,
- "Frauenfeld": null,
- "Frauenhain": null,
- "Frechen-Königsdorf": null,
- "Freckleben": null,
- "Freden(Leine)": null,
- "Fredenbeck": null,
- "Fredericia st": null,
- "Fredersdorf(b Berlin)": null,
- "Freiberg(Neckar)": null,
- "Freiberg(Sachs)": null,
- "Freiburg Klinikum": null,
- "Freiburg Messe/Universität": null,
- "Freiburg(Breisgau) Hbf": null,
- "Freiburg-Herdern": null,
- "Freiburg-Landwasser": null,
- "Freiburg-Littenweiler": null,
- "Freiburg-St Georgen": null,
- "Freiburg-Wiehre": null,
- "Freiburg-Zähringen": null,
- "Freienbach SBB": 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 West": null,
- "Freital-Potschappel": null,
- "Frellstedt": null,
- "Frelsdorf": null,
- "Fremdingen Bf": null,
- "Fremersdorf": null,
- "Frenkendorf-Füllinsdorf": 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,
- "Fribourg/Freiburg": 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,
- "Friedrichsplatz, Kassel": null,
- "Friedrichsruhe(Meck)": null,
- "Friedrichssegen": null,
- "Friedrichstadt": null,
- "Friedrichstal Mitte, Stutensee": null,
- "Friedrichstal Nord, Stutensee": null,
- "Friedrichstal Saint-Riquier-Platz, Stutensee": null,
- "Friedrichstal b Freudenstadt": null,
- "Friedrichstal(Baden)": null,
- "Friedrichsthal(Saar)": null,
- "Friedrichsthal(Saar) Mitte": null,
- "Friedrichsthal(b Bayreuth)": null,
- "Friedrichswalde(bei Eberswalde)": null,
- "Friesach in Kärnten": null,
- "Friesack(Mark)": null,
- "Friesdorf": null,
- "Friesdorf Ost": null,
- "Friesenheim(Baden)": null,
- "Frimmersdorf": null,
- "Frisvadvej st": null,
- "Fritzens-Wattens": null,
- "Fritzlar": null,
- "Frohburg": null,
- "Frommern": null,
- "Fronhausen(Lahn)": null,
- "Frontenex": null,
- "Frose": null,
- "Frouard": null,
- "Frutigen": null,
- "Frömern": null,
- "Fröndenberg": null,
- "Fröttstädt": null,
- "Fulda": null,
- "Fuldatal-Ihringshausen": null,
- "Furschenbach": null,
- "Furth im Wald": null,
- "Furth(b Deisenhofen)": null,
- "Futuroscope": null,
- "Fährbrücke": null,
- "Föderlach": null,
- "Föhren": null,
- "Förbau": null,
- "Förderstedt": null,
- "Förtha(Eisenach)": null,
- "Förtschendorf": null,
- "Fürfurt": null,
- "Fürnitz": 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-Unterfürberg": null,
- "Füssen": null,
- "Gaanderen": null,
- "Gablingen": null,
- "Gadebusch": null,
- "Gaggenau Bf": null,
- "Gaggenau Mercedes-Benz Werk": null,
- "Gagny": null,
- "Gaildorf West": null,
- "Gaillon Aubevoye": null,
- "Gaimersheim": null,
- "Gaißach": null,
- "Gallarate": null,
- "Gamburg(Tauber)": null,
- "Gammertingen": null,
- "Gammertingen Europastraße": null,
- "Gampel-Steg": null,
- "Ganderkesee": null,
- "Gandrange-Amneville": null,
- "Gangloffsömmern": null,
- "Gannat": null,
- "Ganzlin": null,
- "Garbeck": null,
- "Garbenteich": null,
- "Garching(Alz)": null,
- "Gardanne": 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,
- "Gdansk Glowny": null,
- "Gdansk Oliwa": null,
- "Gdansk Wrzeszcz": null,
- "Gdynia Glowna": null,
- "Gebersdorf": null,
- "Gebra(Hainleite)": null,
- "Geeste": null,
- "Geestenseth": null,
- "Geestgottberg": null,
- "Gehlberg": null,
- "Geigant": null,
- "Geilenkirchen": null,
- "Geilhausen": null,
- "Geinberg": 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,
- "Geldermalsen": null,
- "Geldern": null,
- "Geldrop": null,
- "Geleen Oost": null,
- "Geleen-Lutterade": 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,
- "Gelterkinden": null,
- "Gemmingen": null,
- "Gemmingen West": null,
- "Gemona del Friuli": null,
- "Gemünden(Main)": null,
- "Genderkingen": null,
- "Gendorf": null,
- "Gengenbach": null,
- "Genk": null,
- "Gennweiler": null,
- "Genova Piazza Principe": null,
- "Gensingen-Horrweiler": null,
- "Gent St Pieters": null,
- "Gent-Dampoort": null,
- "Gentbrugge": null,
- "Genthin": null,
- "Genève": null,
- "Genève-Aéroport": null,
- "Georgensgmünd": null,
- "Gera Hbf": null,
- "Gera Süd": null,
- "Gera-Langenberg": null,
- "Gera-Zwötzen": null,
- "Geraberg": null,
- "Geradstetten": null,
- "Gerhausen": null,
- "Gerichshain": null,
- "Gerlachsheim": null,
- "Gerlafingen": null,
- "Gerlenhofen": null,
- "Gerling im Pinzgau": null,
- "Germering-Unterpfaffenhofen": null,
- "Germersheim": 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,
- "Gevrey-Chambertin": null,
- "Giengen(Brenz)": null,
- "Giersleben": null,
- "Gießen": null,
- "Gießen Erdkauter Weg": null,
- "Gießen Licher Str": null,
- "Gießen Oswaldsgarten": null,
- "Gießenbach in Tirol": null,
- "Gifhorn": null,
- "Gifhorn Stadt": null,
- "Gilching-Argelsried": null,
- "Gildenhall": null,
- "Gilze-Rijen": null,
- "Gingen(Fils)": null,
- "Girod": null,
- "Girona": null,
- "Gisikon-Root": null,
- "Gisors Embranchement": null,
- "Gittelde/Bad Grund(Harz)": null,
- "Giubiasco": null,
- "Gjesing st": null,
- "Gladbeck Ost": null,
- "Gladbeck West": null,
- "Gladbeck-Zweckel": null,
- "Glan-Münchweiler": null,
- "Glanerbrug": null,
- "Glanzstoffwerke": null,
- "Glashütte(Sachs)": null,
- "Glattbrugg": null,
- "Glattfelden": null,
- "Glaubitz(Riesa)": null,
- "Glauburg-Glauberg": null,
- "Glauburg-Stockheim": null,
- "Glauchau(Sachs)": null,
- "Glauchau-Schönbörnchen": null,
- "Gleisdorf": null,
- "Glesch": null,
- "Gloggnitz": null,
- "Glossen(b Oschatz)": null,
- "Glöwen": null,
- "Glückauf": null,
- "Glückstadt": null,
- "Gmund(Tegernsee)": null,
- "Gmünd NÖ": null,
- "Gnadau": null,
- "Gnarrenburg": null,
- "Gnarrenburg Nord": null,
- "Gnevkow": null,
- "Gniezno": null,
- "Goch": null,
- "Gochsheim(Baden)": null,
- "Godramstein": null,
- "Goebelsmühle": null,
- "Goes": null,
- "Gokels": null,
- "Goldbeck(Osterburg)": null,
- "Goldberg(Württ)": null,
- "Goldenstedt(Oldb)": null,
- "Goldhausen": null,
- "Goldshöfe": null,
- "Golling-Abtenau": null,
- "Gollmitz(Niederlausitz)": null,
- "Golm": null,
- "Golzow(Eberswalde)": null,
- "Golzow(Oderbruch)": null,
- "Golßen(Niederlausitz)": null,
- "Gomadingen": null,
- "Gommern": null,
- "Gondelsheim Schlossstadion": null,
- "Gondelsheim(Baden)": null,
- "Goor": null,
- "Goppenstein": null,
- "Gorgast": null,
- "Gorinchem": null,
- "Gosberg": null,
- "Goslar": null,
- "Gossau SG": null,
- "Gotha": null,
- "Gotha Ost": null,
- "Gottenheim": null,
- "Gottesauer Platz/BGV, Karlsruhe": null,
- "Gotteszell": null,
- "Gottlieben (Schifflände)": null,
- "Gottmadingen": null,
- "Gouda": null,
- "Gouda Goverwelle": null,
- "Gouvy": 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,
- "Graffenstaden": null,
- "Grafing Bahnhof": null,
- "Grafing Stadt": null,
- "Grafling-Arzting": null,
- "Grafrath": null,
- "Gramatneusiedl": null,
- "Grambow": null,
- "Gramsbergen": null,
- "Granollers": null,
- "Gransee": null,
- "Gratwein-Gratkorn": null,
- "Grauschwitz Flocke": null,
- "Graz Hbf": null,
- "Graz Ostbahnhof-Messe": null,
- "Grebenstein": null,
- "Gredstedbro st": null,
- "Greifswald": null,
- "Greifswald Süd": null,
- "Greiz": null,
- "Greiz-Dölau": null,
- "Grenchen Nord": null,
- "Grenoble": null,
- "Grenzach": null,
- "Greppin": null,
- "Gresy-sur-Isere": null,
- "Gretz-Armainvilliers": null,
- "Greußen": null,
- "Greven": null,
- "Grevenbroich": null,
- "Grevesmühlen": null,
- "Grieben(Meckl)": null,
- "Griebo": null,
- "Griefstedt": null,
- "Gries am Brenner": null,
- "Gries im Pinzgau": null,
- "Griesen(Oberbay)": null,
- "Grieskirchen-Gallspach": null,
- "Grießen(Baden)": null,
- "Grijpskerk": null,
- "Grimma ob Bf": null,
- "Grimmen": null,
- "Grimmenthal": null,
- "Grobau": null,
- "Groenendaal": null,
- "Grombach": null,
- "Gronau(Westf)": null,
- "Groningen": null,
- "Groningen Europapark": null,
- "Groningen Noord": null,
- "Gronsdorf": null,
- "Grou-Jirnsum": null,
- "Groß Ammensleben": null,
- "Groß Behnitz": 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ßbodungen": 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,
- "Grunbach": null,
- "Grunow(Niederlausitz)": null,
- "Gräfelfing": null,
- "Gräfenberg": null,
- "Gräfendorf": null,
- "Gräfenhainichen": null,
- "Gräfenroda": null,
- "Gräfenstuhl-Klippmühle": null,
- "Gräfentonna": null,
- "Gräveneck": null,
- "Grävenwiesbach": null,
- "Gröbenzell": null,
- "Gröbers": null,
- "Gröbming": 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,
- "Guldager st": null,
- "Gummersbach": null,
- "Gummersbach-Dieringhausen": null,
- "Gumpenried-Asbach": null,
- "Gundelfingen(Bay)": null,
- "Gundelfingen(Breisgau)": null,
- "Gundelsdorf": null,
- "Gundelshausen": null,
- "Gundelsheim(Neckar)": null,
- "Gundersheim(Rheinhess)": null,
- "Guntersblum": null,
- "Guntramsdorf Kaiserau": null,
- "Gunzenhausen": null,
- "Gurten OÖ": null,
- "Gussenstadt": null,
- "Gustorf": null,
- "Gutach Freilichtmuseum": null,
- "Gutach(Breisgau)": null,
- "Gutenfürst": null,
- "Guthmannshausen": null,
- "Guxhagen": null,
- "Györ": null,
- "Gänserndorf": 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öschenen": null,
- "Götschendorf": null,
- "Göttingen": null,
- "Götz": null,
- "Götzendorf/Leitha": null,
- "Götzis": null,
- "Gößnitz": null,
- "Güdingen": 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ütersloh Hbf": null,
- "Güttingen": null,
- "Haaltert": null,
- "Haan": null,
- "Haan-Gruiten": null,
- "Haar": null,
- "Haarhausen": null,
- "Haarlem": null,
- "Haarlem Spaarnwoude": null,
- "Habsheim(Mulh)": null,
- "Hachenburg": null,
- "Hadamar": null,
- "Hademarschen": null,
- "Hademstorf": null,
- "Hadmersleben": null,
- "Haffkrug": null,
- "Hagebök": null,
- "Hagelstadt": null,
- "Hagen Hbf": null,
- "Hagen(Han)": null,
- "Hagen(Kr. Stade)": null,
- "Hagen-Heubing": null,
- "Hagen-Oberhagen": null,
- "Hagen-Vorhalle": null,
- "Hagen-Wehringhausen": null,
- "Hagen-Westerbauer": null,
- "Hagenau im Innkreis": null,
- "Hagenbach": null,
- "Hagenbüchach": null,
- "Hagenow Land": null,
- "Hagenow Stadt": null,
- "Hagenwerder": null,
- "Hagondange": null,
- "Hagsfeld Bahnhof, Karlsruhe": null,
- "Hagsfeld Geroldsäcker, Karlsruhe": null,
- "Hagsfeld Jenaer Straße, Karlsruhe": null,
- "Hagsfeld Reitschulschlag (Schleife), Karlsruhe": null,
- "Hagsfeld Reitschulschlag, Karlsruhe": null,
- "Hagsfeld Süd, Karlsruhe": null,
- "Haguenau": null,
- "Haidenaab-Göppmannsbühl": null,
- "Haidkapelle": null,
- "Haiger": null,
- "Haiger Obertor": null,
- "Haigerloch": null,
- "Hailer-Meerholz": null,
- "Haiming": 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,
- "Halfweg-Zwanenburg": null,
- "Halitplatz, Kassel": null,
- "Hall in Tirol": 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(S) Heidebf": null,
- "Halle(Saale)Hbf": null,
- "Halle(Westf)": null,
- "Halle(Westf) OWL-Arena": null,
- "Halle-Ammendorf": null,
- "Halle-Neustadt": null,
- "Halle-Nietleben": null,
- "Halle-Silberhöhe": null,
- "Halle-Trotha": null,
- "Hallein": null,
- "Hallstadt(b Bamberg)": null,
- "Hallwang-Elixhausen": null,
- "Halstenbek": null,
- "Haltern am See": null,
- "Haltingen": null,
- "Halver-Oberbrügge": null,
- "Hamburg Airport": null,
- "Hamburg Alte Wöhr": null,
- "Hamburg Berliner Tor": null,
- "Hamburg Billwerder-Moorfleet": null,
- "Hamburg Burgwedel": null,
- "Hamburg Dammtor": null,
- "Hamburg Diebsteich": null,
- "Hamburg Elbbrücken": null,
- "Hamburg Elbgaustraße": null,
- "Hamburg Friedrichsberg": 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 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-Blankenese": 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-Ohlsdorf": null,
- "Hamburg-Othmarschen": null,
- "Hamburg-Poppenbüttel": null,
- "Hamburg-Rahlstedt": null,
- "Hamburg-Rissen": null,
- "Hamburg-Rothenburgsort": null,
- "Hamburg-Schnelsen": 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)Hbf": null,
- "Hamm-Bockum-Hövel": null,
- "Hamm-Heessen": null,
- "Hammah": null,
- "Hammelburg": null,
- "Hammelburg Ost": null,
- "Hammelspring": null,
- "Hammerau": null,
- "Hammersbach Zugspitzbahn, Grainau": null,
- "Hammerstein": null,
- "Hammerunterwiesenthal": null,
- "Hamminkeln": null,
- "Hamminkeln-Dingden": 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,
- "Hanweiler-Bad Rilchingen": null,
- "Happurg": null,
- "Harblek": null,
- "Harburg(Schwab)": null,
- "Hard-Fussach": null,
- "Hardegsen": null,
- "Hardenberg": null,
- "Harderwijk": null,
- "Hardhof": null,
- "Hardinxveld Blauwe Zoom": null,
- "Hardinxveld-Giessendam": null,
- "Haren(Ems)": null,
- "Haren(NL)": null,
- "Harlesiel": null,
- "Harlingen(NL)": 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,
- "Harzgerode": null,
- "Hasbergen": null,
- "Haselbrunn": null,
- "Haselstauden (Dornbirn)": null,
- "Haslach": null,
- "Hasloch(Main)": null,
- "Hasloh": null,
- "Haslohfurth": null,
- "Haspelmoor": null,
- "Hassel(Saar)": null,
- "Hasselborn": null,
- "Hasselfelde": null,
- "Hasselt": null,
- "Haste": null,
- "Hatlerdorf(Dornbirn)": null,
- "Hattenheim": null,
- "Hattersheim(Main)": null,
- "Hattert": null,
- "Hatting in Tirol": 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,
- "Hauptfriedhof, Karlsruhe": null,
- "Hauptfriedhof, Kassel": null,
- "Hauptstuhl": null,
- "Hauptwil": null,
- "Haus Bethlehem, Karlsruhe": null,
- "Haus im Ennstal": 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,
- "Hayange": null,
- "Haynsburg": null,
- "Hazebrouck": 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,
- "Heemskerk": null,
- "Heemstede-Aerdenhout": null,
- "Heerbrugg": null,
- "Heerenveen": null,
- "Heerhugowaard": null,
- "Heerlen": null,
- "Heerlen Woonboulevard": null,
- "Heeze": null,
- "Hegelsbergstraße, Kassel": null,
- "Heggen": null,
- "Hegne": null,
- "Hegyeshalom": null,
- "Hegyeshalom(Gr)": 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 Karlstor": 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,
- "Heiloo": null,
- "Heilsbronn": null,
- "Heimbach (Eifel)": null,
- "Heimbach(Nahe)": null,
- "Heimbach(Nahe)Ort": null,
- "Heimenkirch": null,
- "Heimerdingen": null,
- "Heimersheim": null,
- "Heimstetten": null,
- "Heinebach": null,
- "Heino": null,
- "Heinrich-Heine-Straße, Kassel": 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,
- "Heiterwang-Plansee": null,
- "Heldrungen": null,
- "Helenesee": null,
- "Helmbrechts": null,
- "Helmond": null,
- "Helmond Brandevoort": null,
- "Helmond Brouwhuis": null,
- "Helmond t Hout": null,
- "Helmsdorf(Pirna)": null,
- "Helmsheim": null,
- "Helmstadt(Baden)": null,
- "Helmstedt": null,
- "Helpup": null,
- "Hemmen-Dodewaard": null,
- "Hemmerde": null,
- "Hemmersdorf(Saar)": null,
- "Hemmingen": null,
- "Hemmoor": null,
- "Hemsbach": null,
- "Hemsen(b Soltau)": null,
- "Hendaye": null,
- "Hendschiken": null,
- "Henfenfeld": null,
- "Hengelo": null,
- "Hengelo Gezondheidspark": null,
- "Hengelo Oost": null,
- "Henin-Beaumont": null,
- "Henne st": null,
- "Hennef im Siegbogen": null,
- "Hennef(Sieg)": null,
- "Hennen": null,
- "Hennersdorf(Sachs)": null,
- "Hennigsdorf (S)": 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,
- "Herentals": null,
- "Herford": null,
- "Hergatz": null,
- "Hergenrath": 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,
- "Herny": null,
- "Heroldsberg": null,
- "Heroldsberg Nord": null,
- "Herrath": null,
- "Herrenberg": null,
- "Herrenberg Zwerchweg": null,
- "Herrensee": null,
- "Herrenstraße, Karlsruhe": null,
- "Herrlingen": null,
- "Herrlisheim près Colmar": null,
- "Herrlishöfen": null,
- "Herrnburg": null,
- "Herrsching": null,
- "Hersbruck(l Pegnitz)": null,
- "Hersbruck(r Pegnitz)": null,
- "Herstal": 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,
- "Herzele": null,
- "Herzhorn": null,
- "Herzogenbuchsee": null,
- "Herzogenburg": 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,
- "Hettange Grande": 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,
- "Hildbrandsgrün": null,
- "Hildburghausen": null,
- "Hilden": null,
- "Hilden Süd": null,
- "Hildesheim Hbf": null,
- "Hildesheim Ost": null,
- "Hillegom": null,
- "Hillnhütten": null,
- "Hilpertsau": null,
- "Hilpoltstein": null,
- "Hilter": null,
- "Hilversum": null,
- "Hilversum Media Park": null,
- "Hilversum Sportpark": null,
- "Himmelpforten": null,
- "Himmelreich": null,
- "Himmelstadt": null,
- "Hindeloopen": 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,
- "Hirtenweg/Technologiepark, Karlsruhe": null,
- "Hittfeld": null,
- "Hitzacker": null,
- "Hnevice": null,
- "Hochdahl": null,
- "Hochdahl-Millrath": null,
- "Hochdorf(b Horb)": null,
- "Hochfelden": null,
- "Hochfilzen": null,
- "Hochhausen(Tauber)": null,
- "Hochheim(Main)": null,
- "Hochneukirch": null,
- "Hochspeyer": null,
- "Hochstadt-Marktzeuln": null,
- "Hochstetten": null,
- "Hochstetten Altenheim, Linkenheim-Hochstetten": null,
- "Hochstetten Grenzstraße": null,
- "Hochstetten(Nahe)": null,
- "Hochstätten(Pfalz)": null,
- "Hochwang": null,
- "Hochzirl": null,
- "Hockenheim": null,
- "Hockeroda": null,
- "Hodenhagen": null,
- "Hodonin": null,
- "Hoeilaart": null,
- "Hoeje Taastrup st": null,
- "Hoensbroek": null,
- "Hoevelaken": 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,
- "Hohenau": null,
- "Hohenbrunn": null,
- "Hohendorf": null,
- "Hohenebra Ort": null,
- "Hoheneggelsen": null,
- "Hohenems": 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,
- "Hollandsche Rading": null,
- "Holländische Straße, Kassel": null,
- "Holländischer Platz/Universität, Kassel": null,
- "Holm-Seppensen": null,
- "Holstentherme": null,
- "Holten": null,
- "Holtensen/Linderte": null,
- "Holthusen": null,
- "Holzdorf(Elster)": null,
- "Holzdorf(b Weimar)": null,
- "Holzgerlingen Bf": null,
- "Holzgerlingen Buch": null,
- "Holzgerlingen Hülben": null,
- "Holzhau": null,
- "Holzhau Skilift": null,
- "Holzhausen(Kr Siegen)": null,
- "Holzheim(b Neuss)": null,
- "Holzkirchen": null,
- "Holzminden": null,
- "Holzwickede": null,
- "Hombourg-Haut": null,
- "Homburg(Saar)Hbf": null,
- "Honrath": null,
- "Hoofddorp": null,
- "Hoogeveen": null,
- "Hoogezand-Sappemeer": null,
- "Hoogkarspel": null,
- "Hoorn": null,
- "Hoorn Kersenboogerd": null,
- "Hopfgarten im Brixental": null,
- "Hopfgarten im Brixental Berglift": null,
- "Hopfgarten(Sachs)": null,
- "Hopfgarten(Weimar)": null,
- "Hoppecke": null,
- "Hoppegarten(Mark)": null,
- "Hoppingen": null,
- "Hoppstädten(Nahe)": null,
- "Horb": null,
- "Horb-Heiligenfeld": null,
- "Horgen": null,
- "Horka": null,
- "Horn(Bodensee)": null,
- "Horn(Bodensee), SF": null,
- "Horn-Bad Meinberg": null,
- "Hornberg(Schwarzw)": null,
- "Horneburg": null,
- "Horni Blatna": null,
- "Horni Dvoriste": null,
- "Horni Kamenice": null,
- "Horni Podluzi": null,
- "Horni Poustevna": null,
- "Hornstorf": null,
- "Horovice": null,
- "Horrem": null,
- "Horsens st": null,
- "Horst(Holst)": null,
- "Horst-Sevenum": null,
- "Hosena": null,
- "Houten": null,
- "Houten Castellum": null,
- "Houthem-St. Gerlach": null,
- "Howald": null,
- "Hoyerswerda": null,
- "Hoyerswerda-Neustadt": null,
- "Hoykenkamp": null,
- "Hradek nad Nisou": null,
- "Hranice na Morave": null,
- "Hrebeny": null,
- "Hubacker": null,
- "Hubertushöhe": null,
- "Huchem-Stammeln": null,
- "Huckstorf": null,
- "Hude": null,
- "Hufschlag": null,
- "Huglfing": null,
- "Hugstetten": null,
- "Hulb": null,
- "Hundsgrün": null,
- "Hundstadt": null,
- "Hungen": null,
- "Huntlosen": null,
- "Hurdegaryp": null,
- "Husby": null,
- "Husum": null,
- "Huttenheim": null,
- "Huy(B)": null,
- "Huzenbach": null,
- "Hviding st": null,
- "Hyllerslev st": null,
- "Häggenschwil-Winden": null,
- "Hähnichen": null,
- "Hähnlein-Alsbach": null,
- "Hämelerwald": null,
- "Hämerten": null,
- "Händelstraße, Karlsruhe": null,
- "Hässleholm Central": 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örsching": null,
- "Hörselgau": null,
- "Hörstel": null,
- "Hörstmar(Lippe)": null,
- "Hösbach": null,
- "Hösel": null,
- "Höste": null,
- "Hövelhof": null,
- "Hövelriege": null,
- "Höxter Rathaus": null,
- "Höxter-Godelheim": null,
- "Höxter-Lüchtringen": null,
- "Höxter-Ottbergen": null,
- "Hübschstraße, Karlsruhe": null,
- "Hückelhoven-Baal": null,
- "Hüffenhardt": null,
- "Hüfingen Mitte": null,
- "Hünfeld": null,
- "Hüntwangen-Wil": null,
- "Hürth-Kalscheuren": null,
- "Hüttau": null,
- "Hütten": null,
- "Hüttenbusch": null,
- "Hüttengrund": null,
- "Hüttingen": null,
- "IJlst": 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,
- "Ilawa Glowna": 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,
- "Immensee": null,
- "Immensen-Arpke": null,
- "Immenstadt": null,
- "Imst-Pitztal": null,
- "Imsterberg": null,
- "Imsweiler": null,
- "Ingelbach": null,
- "Ingelheim": null,
- "Ingelmunster": null,
- "Ingolstadt Audi": null,
- "Ingolstadt Hbf": null,
- "Ingolstadt Nord": null,
- "Ingwiller": null,
- "Inheiden": null,
- "Inningen": null,
- "Innsbruck Hbf": null,
- "Innsbruck Hötting": null,
- "Innsbruck Westbahnhof": null,
- "Inowroclaw": null,
- "Inselstadt Malchow": null,
- "Insheim": null,
- "Interlaken Ost": null,
- "Interlaken West": null,
- "Inzing/Inn": null,
- "Iphofen": null,
- "Ipsheim": null,
- "Irfersgrün": null,
- "Irrenlohe": null,
- "Is-sur-Tille": null,
- "Iselle di Trasquera": null,
- "Iselle transito": null,
- "Iserlohn": null,
- "Iserlohnerheide": null,
- "Isernhagen": null,
- "Ismaning": null,
- "Ispringen": null,
- "Isselhorst-Avenwedde": null,
- "Istein": null,
- "Ittersbach Bahnhof": null,
- "Ittersbach Industrie, Karlsbad": null,
- "Ittersbach Rathaus": null,
- "Ittling": null,
- "Ittlingen": null,
- "Itzehoe": null,
- "Itzelberg": null,
- "Ivanic Grad": null,
- "Jabel(Meckl)": null,
- "Jablonne v Podjestedi": null,
- "Jacobsdorf(Mark)": null,
- "Jaderberg": null,
- "Jagdschloß": null,
- "Jagstzell": null,
- "Jahnsdorf(Erzgeb)": null,
- "Janderup st": null,
- "Jankowa Zaganska": null,
- "Janovice nad Uhlavou": null,
- "Jarrenwisch": null,
- "Jasnitz": null,
- "Jatznick": null,
- "Jeber-Bergfrieden": null,
- "Jechtingen": null,
- "Jedlova": null,
- "Jeeser": null,
- "Jegum st": null,
- "Jelenia Gora": null,
- "Jena Paradies": null,
- "Jena Saalbf": null,
- "Jena West": null,
- "Jena-Göschwitz": null,
- "Jena-Zwätzen": null,
- "Jenbach": null,
- "Jenbach Zillertalbahn": null,
- "Jennersdorf": null,
- "Jerichow": null,
- "Jerxheim": null,
- "Jesenice(Gr)": null,
- "Jesenice(SL)": null,
- "Jesewitz(Leipzig)": null,
- "Jessen(Elster)": null,
- "Jestetten": null,
- "Jettenbach": null,
- "Jettingen": null,
- "Jeumont": null,
- "Jever": null,
- "Jeßnitz(Anh)": null,
- "Jiretin pod Jedlovou": null,
- "Jirkov zast.": null,
- "Joachimsthal": null,
- "Joachimsthal Kaiserbahnhof": null,
- "Jocketa": null,
- "Jockgrim Bf": null,
- "Joeuf": null,
- "Johanngeorgenstadt": null,
- "Joigny(Lar.Migennes)": null,
- "Jossa": null,
- "Judenburg": null,
- "Julbach": null,
- "Jungingen(Hohenz)": null,
- "Jungnau": null,
- "Juvisy": null,
- "Jägersfreude": null,
- "Jänschwalde": null,
- "Jänschwalde Ost": null,
- "Jävenitz": null,
- "Jöhlingen": null,
- "Jöhlingen West": null,
- "Jößnitz": null,
- "Jübek": null,
- "Jüchen": null,
- "Jülich": null,
- "Jülich An den Aspen": null,
- "Jülich Forschungszentrum": null,
- "Jülich-Broich": null,
- "Jülich-Nord": null,
- "Jülich-Selgersdorf": null,
- "Jünkerath": null,
- "Jüterbog": null,
- "Jütrichau": null,
- "KIT-Campus Nord Bahnhof, Eggenstein-Leopoldshafen": null,
- "KVG-Betriebshof, Kassel": null,
- "Kaarst IKEA": null,
- "Kaarst Mitte/Holzbüttgen": null,
- "Kaarster Bahnhof": null,
- "Kaarster See": null,
- "Kablow": null,
- "Kadan-Prunerov": null,
- "Kahl Kopp/Heide": null,
- "Kahl(Main)": null,
- "Kahla(Thür)": null,
- "Kaiseraugst": null,
- "Kaisersesch": null,
- "Kaiserslautern Galgenschanze": null,
- "Kaiserslautern Hbf": null,
- "Kaiserslautern Pfaffwerk": null,
- "Kaiserslautern West": null,
- "Kaiserslautern-Hohenecken": null,
- "Kaiserstuhl AG": null,
- "Kalchreuth": null,
- "Kaldenkirchen": null,
- "Kalenborn(Westerw)": null,
- "Kalhausen": null,
- "Kall": null,
- "Kalsdorf b.Graz": null,
- "Kalsow": null,
- "Kaltenberg": null,
- "Kaltenbrunnen im Montafon": null,
- "Kalteneck": null,
- "Kaltenkirchen Süd": null,
- "Kaltenkirchen(Holst)": null,
- "Kalthof(Kr Iserlohn)": null,
- "Kalwang": null,
- "Kamen": null,
- "Kamen-Methler": null,
- "Kamenz(Sachs)": null,
- "Kamp-Bornhofen": null,
- "Kampen Zuid": null,
- "Kampen(NL)": null,
- "Kandel": null,
- "Kandern": null,
- "Kandersteg": null,
- "Kanzem": null,
- "Kapelle-Biezelinge": null,
- "Kapellen-Drusweiler": null,
- "Kapellen-Wevelinghoven": null,
- "Kapen Biosphärenreservat": null,
- "Kapfenberg": null,
- "Kappelrodeck": null,
- "Kappelrodeck Ost": null,
- "Kapsweyer": null,
- "Karlovy Vary": null,
- "Karlovy Vary dolni n.": null,
- "Karlsburg": null,
- "Karlsdorf": null,
- "Karlshagen": null,
- "Karlsruhe Albtalbahnhof": null,
- "Karlsruhe Bahnhofsvorplatz": null,
- "Karlsruhe Durlacher Tor / KIT-Campus Süd": null,
- "Karlsruhe Entenfang": null,
- "Karlsruhe Hbf": null,
- "Karlsruhe Hbf Südausgang": null,
- "Karlsruhe Marktplatz (Kaiserstraße)": null,
- "Karlsruhe Mühlburger Tor (Kaiserallee)": null,
- "Karlsruhe West": 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,
- "Katowice": null,
- "Kattenes": null,
- "Kattenvenne": null,
- "Katzenfurt": null,
- "Katzhütte": null,
- "Katzwang": null,
- "Katzweiler": null,
- "Kaub": null,
- "Kaufbeuren": null,
- "Kaufering": null,
- "Kaulsdorf(Saale)": null,
- "Kautenbach": null,
- "Kavelstorf(Kr Rostock)": null,
- "Kehl": null,
- "Kehlen": null,
- "Kehlhof": null,
- "Keitum": null,
- "Kelenföld": null,
- "Kelkheim": null,
- "Kelkheim-Hornau": null,
- "Kelkheim-Münster": null,
- "Kellmünz": null,
- "Kelsterbach": null,
- "Kematen in Tirol": 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,
- "Kerkrade Centrum": null,
- "Kerkwitz": null,
- "Kersbach": null,
- "Kesswil": null,
- "Kesteren": 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-Ellerbek": null,
- "Kiel-Elmschenhagen": null,
- "Kiel-Hassee CITTI-PARK": null,
- "Kiel-Oppendorf": null,
- "Kiel-Russee": null,
- "Kierspe": null,
- "Kilchberg(CH)": null,
- "Killer": null,
- "Killwangen-Spreitenbach": null,
- "Kindberg": null,
- "Kinding(Altmühltal)": null,
- "Kindsbach": null,
- "Kirch Göns": null,
- "Kirch-Jesar": null,
- "Kirchanschöring": null,
- "Kirchberg in Tirol": null,
- "Kirchberg(Murr)": null,
- "Kirchbichl": null,
- "Kirchdorf(Deister)": null,
- "Kirchdorf/Krems": null,
- "Kirchehrenbach": null,
- "Kirchen": null,
- "Kirchenlaibach": null,
- "Kirchenlamitz Ost": null,
- "Kirchentellinsfurt": null,
- "Kirchgasse, Kassel": 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,
- "Kirchweidach": null,
- "Kirchweyhe": null,
- "Kirchzarten": null,
- "Kirkel": null,
- "Kirn": null,
- "Kirnbach-Grün": null,
- "Kirnsulzbach": null,
- "Kirschbaumwasen": null,
- "Kissing": null,
- "Kittsee": null,
- "Kitzbühel": null,
- "Kitzbühel Hahnenkamm": null,
- "Kitzingen": null,
- "Kißlegg": null,
- "Klaffenbach Hp": null,
- "Klagenfurt Hbf": null,
- "Klais": null,
- "Klandorf": null,
- "Klanxbüll": null,
- "Klarenbeek": null,
- "Klasdorf Glashütte": null,
- "Klatovy": null,
- "Klaus in Vorarlberg": null,
- "Klecken": null,
- "Kledering b.Wien": null,
- "Klein Bünzow": null,
- "Klein Gerau": null,
- "Klein Winternheim-Ober Olm": null,
- "Kleinberghofen": null,
- "Kleinbettingen": null,
- "Kleinblittersdorf": null,
- "Kleinenbroich": null,
- "Kleinensiel": 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,
- "Klimmen-Ransdaal": null,
- "Klinge": null,
- "Klingenberg(Main)": null,
- "Klingenberg-Colmnitz": null,
- "Klingenbrunn": null,
- "Klingenthal": null,
- "Klingnau": null,
- "Klinikum Bremen-Nord/Beckedorf": null,
- "Klitschmar": null,
- "Klitten": null,
- "Kloster Bronnbach": null,
- "Kloster Marienthal": null,
- "Kloster Oesede": null,
- "Klosterbuch": null,
- "Klosterfelde": null,
- "Klosterlechfeld": null,
- "Klostermansfeld": null,
- "Klostermansfeld Randsiedlung": null,
- "Klosterreichenbach": null,
- "Kloten": null,
- "Klotten": null,
- "Kläden(Stendal)": null,
- "Knesebeck": null,
- "Knielingen Eggensteiner Straße, Karlsruhe": null,
- "Knielingen Herweghstraße, Karlsruhe": null,
- "Knielingen Siemens, Karlsruhe": null,
- "Knielinger Allee/Städt. Klinikum, Karlsruhe": null,
- "Knittelfeld": null,
- "Knittlingen-Kleinvillars": null,
- "Knöringen-Essingen": null,
- "Kobern-Gondorf": null,
- "Koblenz Dorf": null,
- "Koblenz Hbf": null,
- "Koblenz Stadtmitte": null,
- "Koblenz(CH)": null,
- "Koblenz-Ehrenbreitstein": null,
- "Koblenz-Güls": null,
- "Koblenz-Lützel": null,
- "Koblenz-Moselweiß": null,
- "Kochel": null,
- "Kodersdorf": null,
- "Koebenhavn H": null,
- "Koebenhavns Lufthavn st": null,
- "Kogenheim": null,
- "Kohlscheid": null,
- "Kohlstetten": null,
- "Kolbermoor": null,
- "Kolbnitz": null,
- "Kolding st": null,
- "Kolin(CZ)": null,
- "Kolkwitz": null,
- "Kolkwitz Süd": null,
- "Kollmarsreute": null,
- "Kollnau": null,
- "Komarom": null,
- "Konin": null,
- "Konstanz": null,
- "Konstanz Hafen": null,
- "Konstanz-Fürstenberg": null,
- "Konstanz-Petershausen": null,
- "Konstanz-Wollmatingen": null,
- "Konz": null,
- "Konz Mitte": null,
- "Konzerthaus, Karlsruhe": null,
- "Koog aan de Zaan": null,
- "Korbach Hbf": null,
- "Korbach Süd": null,
- "Kordel": null,
- "Kork": null,
- "Korntal": null,
- "Korntal Gymnasium": null,
- "Kornwestheim Pbf": null,
- "Korschenbroich": null,
- "Korsoer st": null,
- "Kortenberg": null,
- "Kortrijk": null,
- "Koserow": null,
- "Kothmaißling": null,
- "Kottenheim": null,
- "Koudum-Molkwerum": null,
- "Krabbendijke": null,
- "Kraftsdorf": null,
- "Kraftwerk Finkenheerd": null,
- "Kraghammer": null,
- "Krakow Glowny": null,
- "Kralupy nad Vltavou": null,
- "Kranebitten": null,
- "Kranichfeld": null,
- "Kranj": null,
- "Kraslice": null,
- "Kraslice predmesti": null,
- "Kraslice-Pod vlekem": null,
- "Krasna Lipa": null,
- "Krasna Lipa mesto": null,
- "Kratzeburg": null,
- "Krauthausen": null,
- "Kredenbach": null,
- "Krefeld Hbf": null,
- "Krefeld-Hohenbudberg Chempark": null,
- "Krefeld-Linn": null,
- "Krefeld-Oppum": null,
- "Krefeld-Uerdingen": null,
- "Kreiensen": null,
- "Kreimbach-Kaulbach": null,
- "Kremmen": null,
- "Krempe": null,
- "Kremperheide": null,
- "Krems an der Donau": null,
- "Krensitz": null,
- "Kressbronn": null,
- "Kressbronn Hafen": null,
- "Kretscham-Rothensehma": null,
- "Kreuz Konz": null,
- "Kreuzau Bahnhof": null,
- "Kreuzau-Eifelstraße": null,
- "Kreuzberg(Ahr)": null,
- "Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc": null,
- "Kreuzlingen": null,
- "Kreuzlingen Bernrain": null,
- "Kreuzlingen Hafen": null,
- "Kreuzstraße": null,
- "Kreuztal": null,
- "Kreuztal-Littfeld": null,
- "Kriftel": null,
- "Krimmeri-Meinau": null,
- "Krimov": null,
- "Krippen": null,
- "Krommenie-Assendelft": null,
- "Kronach": null,
- "Kronberg Süd": null,
- "Kronberg(Taunus)": null,
- "Kronenplatz (Fritz-Erler-Str.), Karlsruhe": null,
- "Kronenplatz (Kaiserstraße), Karlsruhe": null,
- "Kronshagen": null,
- "Kronskamp": null,
- "Kronweiler": null,
- "Kropswolde": null,
- "Krsko": null,
- "Kruft": null,
- "Kruiningen-Yerseke": null,
- "Krumbach(Schwab)": null,
- "Krumbach(Schwab)Schule": null,
- "Krumhermsdorf": null,
- "Krumpa": null,
- "Krumpendorf/Wörthersee": null,
- "Krupunder": null,
- "Krzewina Zgorzelecka": null,
- "Krölpa-Ranis": null,
- "Kröpelin": null,
- "Kubschütz": null,
- "Kuchen": null,
- "Kuchl": null,
- "Kufstein": null,
- "Kullenmühle, Bad Herrenalb": null,
- "Kulmbach": null,
- "Kummerow(Stralsund)": null,
- "Kummersdorf(Storkow)": null,
- "Kundl": null,
- "Kunersdorf": null,
- "Kunowice": null,
- "Kupfermühle": null,
- "Kuppenheim": null,
- "Kurort Altenberg(Erzgebirge)": null,
- "Kurort Jonsdorf": null,
- "Kurort Jonsdorf Hst": null,
- "Kurort Kipsdorf": null,
- "Kurort Oberwiesenthal": null,
- "Kurort Oybin": null,
- "Kurort Oybin-Niederdorf": null,
- "Kurort Rathen": null,
- "Kurt-Schumacher-Straße, Karlsruhe": null,
- "Kusel": null,
- "Kutenholz": null,
- "Kutina": null,
- "Kutno": null,
- "Kuty": null,
- "Kutzenhausen": null,
- "Kyhna": null,
- "Kyllburg": null,
- "Kyritz": null,
- "Kytlice": 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-Holweide": null,
- "Köln-Longerich": null,
- "Köln-Mülheim": null,
- "Köln-Müngersdorf Technologiepark": 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önigsplatz, Kassel": 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örmend": null,
- "Köthen": null,
- "Köttewitz": null,
- "Kötzschau": null,
- "Kövenig": null,
- "Kühler Krug, Karlsruhe": null,
- "Kühnhausen": null,
- "Kühren": null,
- "Külte-Wetterburg": null,
- "Künsebeck": null,
- "Küntrop": null,
- "Küps": null,
- "Kürbitz": null,
- "Küssnacht am Rigi": null,
- "Küstrin-Kietz": null,
- "LAigle": 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,
- "Laaber": null,
- "Laage(Meckl)": null,
- "Laberweinting": null,
- "Lachen": null,
- "Ladenburg": null,
- "Lage Zwaluwe": null,
- "Lage(Lippe)": null,
- "Lagerlechfeld": null,
- "Lahntal-Sarnau": null,
- "Lahr(Schwarzw)": null,
- "Laineck": null,
- "Lalendorf": null,
- "Lam": null,
- "Lamadelaine": null,
- "Lambach": null,
- "Lambrecht(Pfalz)": null,
- "Lambsheim": null,
- "Lameyplatz, Karlsruhe": null,
- "Lamone-Cadempino": 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,
- "Landeck-Zams": null,
- "Landen": null,
- "Landgraaf": null,
- "Landquart": null,
- "Landry": 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,
- "Langdorp": null,
- "Langebrück(Sachs)": null,
- "Langeln(Holst)": null,
- "Langelsheim": null,
- "Langen am Arlberg": 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,
- "Langensteinbach Schießhüttenäcker, Karlsbad": null,
- "Langensteinbach St. Barbara, Karlsbad": null,
- "Langenthal(CH)": null,
- "Langenwang(Schwab)": null,
- "Langenweddingen": null,
- "Langenwolmsdorf": null,
- "Langenwolmsdorf Mitte": null,
- "Langenzenn": null,
- "Langerwehe": null,
- "Langhagen": null,
- "Langkampfen": null,
- "Langlau": null,
- "Langsdorf(Oberhess)": null,
- "Langwedel": null,
- "Langweid(Lech)": null,
- "Lansingerland-Zoetermeer": 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(CH)": null,
- "Laufen(Oberbay)": null,
- "Laufenburg(Baden)": null,
- "Laufenburg(Baden)Ost": null,
- "Laufenburg(CH)": null,
- "Lauffen(Neckar)": null,
- "Lauingen": null,
- "Laupheim Stadt": null,
- "Laupheim West": null,
- "Laurenburg(Lahn)": null,
- "Lausanne": null,
- "Lausanne-Flon": null,
- "Lauscha(Thür)": null,
- "Lausen(CH)": null,
- "Lauta(Nl)": null,
- "Lautenbach(Baden)": null,
- "Lauter(Sachs)": null,
- "Lauterach": null,
- "Lauterbach Mole": null,
- "Lauterbach(Hess)Nord": null,
- "Lauterbach(Rügen)": null,
- "Lauterbach-Steinbach": null,
- "Lauterbourg": null,
- "Lauterecken-Grumbach": null,
- "Laußnitz": null,
- "Laveno Mombello": null,
- "Le Blanc-Mesnil": null,
- "Le Bourget": null,
- "Le Creusot Montceau Montchanin TGV": null,
- "Le Havre": null,
- "Le Mans": null,
- "Le Raincy Villemomble Montferm": null,
- "Lebach": null,
- "Lebach-Jabach": null,
- "Lebbeke": null,
- "Leer(Ostfriesl)": null,
- "Leerdam": null,
- "Leese-Stolzenau": null,
- "Leeuwarden": null,
- "Leeuwarden Camminghaburen": null,
- "Legden": null,
- "Legefeld": null,
- "Legelshurst": null,
- "Legnica": null,
- "Lehmen": null,
- "Lehndorf(Altenburg)": null,
- "Lehnheim": null,
- "Lehnitz": null,
- "Lehrte": null,
- "Leibnitz": null,
- "Leichlingen": null,
- "Leiden Centraal": null,
- "Leiden Lammenschans": 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 Essener Straße": 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 Miltitzer Allee": null,
- "Leipzig Mockauer Straße": 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-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-Plagwitz": null,
- "Leipzig-Rückmarsdorf": null,
- "Leipzig-Sellerhausen": null,
- "Leipzig-Stötteritz": null,
- "Leipzig-Thekla": null,
- "Leipzig-Wahren": null,
- "Leipzig/Halle Flughafen": null,
- "Leipziger Platz, Kassel": null,
- "Leipziger Straße, Kassel": null,
- "Leisnig": null,
- "Leithen b.Seefeld": null,
- "Leitstade": null,
- "Leißling": null,
- "Lelystad Centrum": null,
- "Lembeck": null,
- "Lemförde": null,
- "Lemgo": null,
- "Lemgo-Lüttfeld": null,
- "Lemmie": null,
- "Lend": null,
- "Lendringsen": null,
- "Lengede-Broistedt": null,
- "Lengefeld-Rauenstein": null,
- "Lengenfeld(Vogtl)": null,
- "Lengenwang": null,
- "Lengerich(Westf)": null,
- "Lenggries": null,
- "Lenglern": null,
- "Lengwil": null,
- "Lennestadt-Altenhundem": null,
- "Lennestadt-Grevenbrück": null,
- "Lennestadt-Meggen": null,
- "Lens(F)": null,
- "Lensahn": null,
- "Lentföhrden": null,
- "Lenzburg": null,
- "Lenzing": null,
- "Leoben Hbf": null,
- "Leogang": null,
- "Leonberg": null,
- "Leopoldsburg": null,
- "Leopoldshafen Frankfurter Straße, Eggenstein-Leopo": null,
- "Leopoldshafen Leopoldstr.": null,
- "Leopoldshafen Viermorgen, Eggenstein-Leopoldshafen": null,
- "Leopoldstal": null,
- "Lermoos": null,
- "Lerouville": null,
- "Les Arcs Draguignan": null,
- "Les-Aubrais-Orleans": null,
- "Lesce-Bled": null,
- "Leschede": null,
- "Lessingstraße, Karlsruhe": null,
- "Letmathe": null,
- "Letmathe Dechenhöhle": null,
- "Letschin": null,
- "Lette(Kr Coesfeld)": null,
- "Letter": null,
- "Leubingen": null,
- "Leubsdorf(Rhein)": null,
- "Leubsdorf(Sachs)": null,
- "Leudelange": null,
- "Leuk": null,
- "Leun/Braunfels": null,
- "Leuna Werke Nord": null,
- "Leuna Werke Süd": null,
- "Leutenberg": null,
- "Leuterschach": null,
- "Leutershausen-Wiedersbach": null,
- "Leutesdorf(Rhein)": null,
- "Leuthen(Cottbus)": null,
- "Leutkirch": null,
- "Leuven": null,
- "Leverkusen Chempark": null,
- "Leverkusen Mitte": null,
- "Leverkusen-Küppersteg": null,
- "Leverkusen-Rheindorf": null,
- "Leverkusen-Schlebusch": null,
- "Lezignan": null,
- "Liberec": null,
- "Libramont": 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,
- "Lichtenvoorde-Groenlo": null,
- "Liebenau(Bz Kassel)": null,
- "Liebenthal(Prignitz)": null,
- "Lieblos": null,
- "Liederbach": null,
- "Liederbach-Süd": null,
- "Lienz in Osttirol": null,
- "Liers": null,
- "Liestal": null,
- "Lietzow(Rügen)": null,
- "Liezen": null,
- "Lille Europe": null,
- "Lille Flandres": null,
- "Limbach(Vogtl)": null,
- "Limbach(b Homburg,Saar)": null,
- "Limburg Süd": null,
- "Limburg(Lahn)": null,
- "Limburgerhof": null,
- "Limmritz(Sachs)": null,
- "Limone": null,
- "Linda(Elster)": null,
- "Lindach": null,
- "Lindau-Aeschach": null,
- "Lindau-Insel": null,
- "Lindau-Reutin": null,
- "Lindenberg(Mark)": null,
- "Lindenberg, Kassel": null,
- "Lindenholzhausen": null,
- "Lindern": null,
- "Lindhorst(Schaumb-Lippe)": null,
- "Lindow(Mark)": null,
- "Lindwedel": null,
- "Lingen(Ems)": null,
- "Lingenfeld": null,
- "Linkenheim Friedrichstraße, Linkenheim-Hochstetten": null,
- "Linkenheim Rathaus": null,
- "Linkenheim Schulzentrum, Linkenheim-Hochstetten": null,
- "Linkenheim Süd, Linkenheim-Hochstetten": null,
- "Linköping Central": null,
- "Linnich Bhf": null,
- "Linnich-Tetz": null,
- "Linsburg": null,
- "Linsenhofen": null,
- "Linz Hbf": null,
- "Linz(Rhein)": null,
- "Linz/Donau Wegscheid": null,
- "Lipinki Luzyckie": null,
- "Lipova u Sluknova": null,
- "Lippstadt": null,
- "Lispenhausen": null,
- "Lissendorf": null,
- "Listerscheid": null,
- "Litija": null,
- "Litomerice mesto": null,
- "Livorno Centrale": null,
- "Liège-Guillemins": null,
- "Ljubljana": null,
- "Lobstädt": null,
- "Locarno": null,
- "Lochau-Hörbranz": null,
- "Lochem": null,
- "Lochham": null,
- "Loeftgaard st": null,
- "Lohgarten-Roth": null,
- "Lohhof": null,
- "Lohmen": null,
- "Lohne(Oldb)": null,
- "Lohnweiler": null,
- "Lohr Bahnhof": null,
- "Lohsa": null,
- "Loitsch-Hohenleuben": null,
- "Lollar": null,
- "Longueau": null,
- "Longwy": null,
- "Lons-Le-Saunier": null,
- "Lonsee": null,
- "Loosdorf b.Melk": null,
- "Loppenhausen": null,
- "Loppersum": null,
- "Lorch(Rhein)": null,
- "Lorch(Württ)": null,
- "Lorchhausen": null,
- "Lorraine": null,
- "Lorsbach": null,
- "Lorsch": null,
- "Lorüns": null,
- "Lottschesee": null,
- "Lottstetten": null,
- "Lourches": null,
- "Lourdes": null,
- "Lovosice": null,
- "Loxstedt": null,
- "Loßburg-Rodt": null,
- "Luban Sl.": null,
- "Lubolz": null,
- "Luckaitztal": null,
- "Luckau-Uckro": null,
- "Luckenau": null,
- "Luckenwalde": null,
- "Ludersheim": null,
- "Ludesch": null,
- "Ludwigsau-Friedlos": null,
- "Ludwigsburg": null,
- "Ludwigschorgast": null,
- "Ludwigsfelde": null,
- "Ludwigsfelde-Struveshof": null,
- "Ludwigshafen(Bodensee)": 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(Rhein) Oppau": null,
- "Ludwigshafen-Mundenheim": null,
- "Ludwigshafen-Oggersheim": null,
- "Ludwigshafen-Rheingönheim": null,
- "Ludwigshöhe": null,
- "Ludwigslust": null,
- "Ludwigsstadt": null,
- "Ludwigsthal": null,
- "Lugano": null,
- "Luh nad Svatavou": null,
- "Luhe": null,
- "Luhe-Wildenau": null,
- "Luino": null,
- "Luisenthal(Saar)": null,
- "Lumes Halte": null,
- "Lund Central": null,
- "Lunde J st": null,
- "Lunden": null,
- "Lunderskov st": null,
- "Lunel": null,
- "Lunestedt": null,
- "Lunteren": null,
- "Lunéville": null,
- "Lupfig": null,
- "Lustenau": null,
- "Luterbach-Attisholz": null,
- "Lutherplatz, Kassel": null,
- "Lutherstadt Eisleben": null,
- "Lutherstadt Wittenberg Altstadt": null,
- "Lutherstadt Wittenberg Hbf": null,
- "Lutherstadt Wittenberg-Labetz": null,
- "Lutherstadt Wittenberg-Piesteritz": null,
- "Lutten": null,
- "Lutterbach": null,
- "Lutum": null,
- "Lutzelbourg": null,
- "Luxembourg": null,
- "Luzern": null,
- "Lyon Part Dieu": null,
- "Lähn": null,
- "Läufelfingen": 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ököshaza": 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ö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üdenscheid": null,
- "Lüdenscheid-Brügge": null,
- "Lüdersdorf(Meckl)": null,
- "Lüdinghausen": null,
- "Lügde": null,
- "Lüneburg": null,
- "Lünen Hbf": null,
- "Lünen-Preußen": null,
- "Lünern": null,
- "Lüssow(Meckl)": null,
- "Lütter": null,
- "Lützel": null,
- "Lützow": null,
- "Maarheeze": null,
- "Maarn": null,
- "Maarssen": null,
- "Maasbüll(b Niebüll)": null,
- "Maastricht": null,
- "Maastricht Noord": null,
- "Maastricht Randwyck": null,
- "Machern(Sachs)": null,
- "Machnin": null,
- "Machnin hrad": 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-Salbke": null,
- "Magdeburg-Sudenburg": null,
- "Magstadt": null,
- "Mahlow": null,
- "Mahlwinkel": null,
- "Maichingen": null,
- "Maichingen Nord": null,
- "Maienfeld": 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 Waggonfabrik": null,
- "Mainz-Bischofsheim": null,
- "Mainz-Gonsenheim": null,
- "Mainz-Gustavsburg": null,
- "Mainz-Kastel": null,
- "Mainz-Laubenheim": null,
- "Mainz-Marienborn": null,
- "Mainz-Mombach": null,
- "Maisach": null,
- "Maishofen-Saalbach": null,
- "Maizieres-les-Metz": null,
- "Mala Velen": null,
- "Malbork": null,
- "Malchin": null,
- "Malching(Oberbay)": null,
- "Malczyce": null,
- "Malk Göhren": null,
- "Mallersdorf": null,
- "Malliß": null,
- "Mallnitz-Obervellach": null,
- "Malmsheim": null,
- "Malmö Central": null,
- "Malsch": null,
- "Malsch Süd": null,
- "Malsfeld": null,
- "Malsfeld-Beiseförth": null,
- "Malter": null,
- "Mamer": null,
- "Mamer Lycée": null,
- "Mammendorf": null,
- "Mammern URh": null,
- "Mammern(Bodensee)": null,
- "Manage": null,
- "Mandern": null,
- "Manebach": null,
- "Manndorf": null,
- "Mannenbach URh": null,
- "Mannenbach-Salenstein": 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,
- "Mansfeld(Südharz)": null,
- "Manternach": null,
- "Mantgum": 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,
- "Marbehan": null,
- "Marburg Süd": null,
- "Marburg(Lahn)": null,
- "Marche-les-Dames": null,
- "Marchegg": null,
- "Marchienne au Pont": null,
- "Marchtrenk": null,
- "Margertshausen Bf": null,
- "Maria Rain": null,
- "Maria Veen": null,
- "Maribor": null,
- "Marienberg(NL)": null,
- "Marienborn": null,
- "Marienhafe": null,
- "Marienheide": null,
- "Markdorf(Baden)": null,
- "Marke": null,
- "Markelfingen": null,
- "Markelsheim": null,
- "Markkleeberg": null,
- "Markkleeberg Mitte": 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,
- "Marktplatz, Karlsruhe": null,
- "Marktredwitz": null,
- "Marktschorgast": null,
- "Markvartice": null,
- "Marl Mitte": null,
- "Marl-Hamm": null,
- "Marl-Sinsen": null,
- "Marle-sur-Serre": null,
- "Marlishausen": null,
- "Marloie": null,
- "Marne la Vallée-Chessy": null,
- "Marnheim": null,
- "Marquardt": null,
- "Marsberg": null,
- "Marseille-Blancarde": null,
- "Marseille-St-Charles": null,
- "Marstetten-Aitrach": null,
- "Martensdorf": null,
- "Martenshoek": null,
- "Martigny": null,
- "Martigues": null,
- "Martinlamitz": null,
- "Martinroda": null,
- "Martinstein": null,
- "Martinszell(Allgäu)": null,
- "Marxgrün": null,
- "Marxzell": null,
- "Marzling": null,
- "Maschen": null,
- "Maselheim": null,
- "Massen": null,
- "Massing": null,
- "Mathystraße, Karlsruhe": null,
- "Matrei am Brenner": null,
- "Matzenbach": null,
- "Matzing": null,
- "Maubach": null,
- "Maubeuge": null,
- "Mauer(b Heidelberg)": null,
- "Maulbronn Stadt/Kloster": null,
- "Maulbronn West": null,
- "Maulburg": null,
- "Mausheim": null,
- "Mautern im Liesingtal": 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,
- "Mayrhofen im Zillertal": null,
- "Mayschoß": null,
- "Mechelen": 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,
- "Medias": null,
- "Meeder": null,
- "Meerane": null,
- "Meerbusch-Osterath": null,
- "Meerssen": null,
- "Meeschensee": null,
- "Mehltheuer": null,
- "Mehrhoog": null,
- "Meine": null,
- "Meinersdorf(Erzgeb)": null,
- "Meinersen": null,
- "Meinerzhagen": null,
- "Meiningen": null,
- "Meiringen": null,
- "Meisdorf": null,
- "Meitingen": null,
- "Meitzendorf": null,
- "Meißen": null,
- "Meißen Altstadt": null,
- "Meißen Triebischtal": null,
- "Melbach": null,
- "Melchow": null,
- "Meldorf": null,
- "Melk": null,
- "Melle": null,
- "Mellenbach-Glasbach": null,
- "Mellendorf": null,
- "Mellikon": null,
- "Mellingen(Thür)": null,
- "Mellrichstadt Bf": null,
- "Mels": null,
- "Melsdorf": null,
- "Melsungen": null,
- "Melsungen Bartenwetzerbrücke": null,
- "Melsungen-Röhrenfurth": null,
- "Melun": null,
- "Memmingen": null,
- "Menden(Rheinl)": null,
- "Menden(Sauerland)": null,
- "Menden(Sauerland)Süd": null,
- "Mendig": null,
- "Mendrisio": null,
- "Mengen": null,
- "Mengeringhausen": null,
- "Mengersgereuth-Hämmern": null,
- "Mengersgereuth-Hämmern Ost": null,
- "Menningen-Leitishofen": null,
- "Menton": null,
- "Menzingen(Baden)": null,
- "Menznau": null,
- "Meppel": null,
- "Meppen": null,
- "Merano/Meran": null,
- "Merching": null,
- "Merchtem": null,
- "Merchweiler": null,
- "Merelbeke": null,
- "Mering": null,
- "Mering-St Afra": null,
- "Mersch(LUX)": null,
- "Mersch(Westf)": null,
- "Merseburg Bergmannsring": null,
- "Merseburg Hbf": null,
- "Merten(Sieg)": null,
- "Mertert": null,
- "Mertesheim": null,
- "Mertingen Bahnhof": null,
- "Merxheim(Colmar)": null,
- "Merzenich": null,
- "Merzig(Saar)": null,
- "Merzig(Saar) Ost": 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,
- "Metz Nord": null,
- "Metz Ville": null,
- "Metzingen(Württ)": null,
- "Metzingen-Neuhausen": null,
- "Meuse TGV": null,
- "Meuselbach-Schwarzmühle": null,
- "Meyenburg": null,
- "Meßdorf": null,
- "Meßkirch": null,
- "Michelau(LUX)": null,
- "Michelau(Oberfr)": null,
- "Michelau(Württ)": null,
- "Michelaubrück": null,
- "Michelbach(Unterfr)": null,
- "Micheldorf": null,
- "Michelstadt": null,
- "Michendorf": null,
- "Middelburg": null,
- "Miedelsbach-Steinenberg": null,
- "Miekinia": null,
- "Miesbach": null,
- "Miesenbach": null,
- "Miesenheim": null,
- "Mieste": null,
- "Miesterhorst": null,
- "Mikulasovice dolni nadrazi": null,
- "Milano Centrale": null,
- "Milano Greco Pirelli": null,
- "Milano Lambrate": null,
- "Milano Porta Garibaldi": null,
- "Millingen(b Rees)": null,
- "Millingen(b Rheinb)": null,
- "Milmersdorf": null,
- "Milmort": null,
- "Miltach": null,
- "Miltenberg": null,
- "Miltern": null,
- "Miltzow": null,
- "Mimberg": null,
- "Mimon": null,
- "Mindelaltheim": null,
- "Mindelheim": null,
- "Minden(Westf)": null,
- "Mining": null,
- "Miramas": null,
- "Mirow": null,
- "Mistorf": null,
- "Mittel Gründau": null,
- "Mittelherwigsdorf": null,
- "Mitteloelsnitz": null,
- "Mittelschmalkalden": null,
- "Mittelsinn": null,
- "Mittenwald": null,
- "Mitterberghütten": null,
- "Mitterdorf-Veitsch": null,
- "Mittergars": null,
- "Mittweida": null,
- "Mixdorf": null,
- "Mixnitz Bärenschützklamm": null,
- "Mlada Boleslav hl.n.": null,
- "Mlyny(CZ)": null,
- "Mochenwangen": null,
- "Mockrehna": null,
- "Modane": null,
- "Moers": null,
- "Moidentin": null,
- "Mol": null,
- "Mols": null,
- "Moltkestraße/Städt. Klinikum, Karlsruhe": null,
- "Mommenheim": null,
- "Monaco-Monte-Carlo": null,
- "Monbach-Neuhausen": null,
- "Monguelfo-Casies/Welsberg-Gsies": null,
- "Monreal": null,
- "Mons": null,
- "Monsheim": null,
- "Montabaur": null,
- "Montbéliard Ville": null,
- "Montelimar": null,
- "Monthey": null,
- "Montluçon Ville": null,
- "Montmelian": null,
- "Montpellier Saint-Roch": null,
- "Montreux": null,
- "Monza": null,
- "Monzingen": null,
- "Mook-Molenhoek": null,
- "Moorbekhalle": null,
- "Moosbachtal": null,
- "Moosbierbaum-Heiligeneich": null,
- "Moosburg": null,
- "Moosrain": null,
- "Moret-Veneux-les-Sablons": null,
- "Morges": null,
- "Morhange": null,
- "Moritzburg": null,
- "Morlesau": null,
- "Morsum": null,
- "Mosbach West": null,
- "Mosbach(Baden)": null,
- "Mosbach-Neckarelz": null,
- "Mosel": null,
- "Moselkern": null,
- "Mosonmagyarovar": null,
- "Most": null,
- "Mouchard": null,
- "Moulins-sur-Allier": null,
- "Mouscron": null,
- "Moustier": null,
- "Moutier": null,
- "Moutiers-Salins-Brides-les-Bains": null,
- "Moyeuvre-Grande": null,
- "Mudersbach": null,
- "Muggensturm": null,
- "Muggensturm Badesee": null,
- "Muhr a See": null,
- "Muizen": null,
- "Mulda(Sachs)": null,
- "Muldenberg": null,
- "Muldenberg Floßplatz": null,
- "Muldenhütten": null,
- "Muldenstein": null,
- "Mulhouse Ville": null,
- "Mulhouse-Dornach": null,
- "Mulsum-Essel": null,
- "Munderkingen": null,
- "Mundolsheim": null,
- "Munkzwalm": null,
- "Munsbach": null,
- "Munster(Metzeral)": null,
- "Munster(Örtze)": null,
- "Muolen": null,
- "Murg(Baden)": null,
- "Murg(CH)": null,
- "Murnau": null,
- "Murnau Ort": null,
- "Murrhardt": null,
- "Musau": null,
- "Mussidan": null,
- "Muttenz": null,
- "Mußbach": null,
- "Mâcon Ville": null,
- "Mâcon-Loché TGV": null,
- "Mägdesprung": null,
- "Mägerkingen": null,
- "Märwil": null,
- "Möckmühl": null,
- "Mögelin": null,
- "Mögglingen(Gmünd)": null,
- "Möhlin": 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örsch Am Hang, Rheinstetten": null,
- "Mörsch Bach-West, Rheinstetten": null,
- "Mörsch Merkurstraße, Rheinstetten": null,
- "Mörsch Narzissenstraße, Rheinstetten": null,
- "Mörsch Rheinaustraße, Rheinstetten": null,
- "Mörsch Römerstraße, Rheinstetten": null,
- "Mörsch Rösselsbrünnle, Rheinstetten": null,
- "Möser": null,
- "Mössingen": null,
- "Möttingen": null,
- "Mötz": null,
- "Mücheln(Geiseltal)": null,
- "Mücheln(Geiseltal) Stadt": null,
- "Mücka": null,
- "Mücke(Hess)": null,
- "Müden(Mosel)": null,
- "Mügeln Bf": null,
- "Mühlacker": null,
- "Mühlacker Rößlesweg": null,
- "Mühlanger": null,
- "Mühlbach(Pirna)": null,
- "Mühlburg West, Karlsruhe": null,
- "Mühlburger Feld, Karlsruhe": null,
- "Mühlburger Tor (Kaiserallee), Karlsruhe": null,
- "Mühldorf(Oberbay)": null,
- "Mühldorf-Möllbrücke": null,
- "Mühlehorn": 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 am Inn": 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 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 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-Perlach": null,
- "München-Riem": null,
- "München-Solln": null,
- "München-Trudering": null,
- "München-Untermenzing": null,
- "München-Westkreuz": null,
- "Münchenbuchsee": 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ünsingen(CH)": null,
- "Münster(Hessen)": null,
- "Münster(W)Zentrum Nord": null,
- "Münster(Westf)Hbf": null,
- "Münster-Albachten": null,
- "Münster-Amelsbüren": null,
- "Münster-Hiltrup": null,
- "Münster-Häger": null,
- "Münster-Mecklenbeck": null,
- "Münster-Roxel": null,
- "Münster-Sarmsheim": null,
- "Münster-Sprakel": null,
- "Münster-Wiesing": null,
- "Münsterlingen-Scherzingen": null,
- "Münstertal(Schwarzwald)": null,
- "Münzesheim": null,
- "Münzesheim Ost": null,
- "Mürlenbach": null,
- "Mürzzuschlag": null,
- "Müssen": null,
- "Naarden-Bussum": null,
- "Nabburg": null,
- "Nachterstedt-Hoym": null,
- "Nackenheim": null,
- "Nagold": null,
- "Nagold Stadtmitte": null,
- "Nagold-Iselshausen": null,
- "Nagold-Steinberg": null,
- "Nagymaros-Visegrad": null,
- "Naila": null,
- "Namborn": null,
- "Namedy": null,
- "Nammen-Bad": null,
- "Namur": null,
- "Nancois Tronville": null,
- "Nancy": null,
- "Nantes": null,
- "Narbonne": 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,
- "Naunhof": null,
- "Neanderthal": null,
- "Nebikon": 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 Mitte": null,
- "Neckarsulm Nord": null,
- "Neckarsulm Süd": null,
- "Neckarzimmern": null,
- "Nedlitz": null,
- "Neef": null,
- "Neerpelt": null,
- "Neetzendorf": null,
- "Neetzka": null,
- "Neheim-Hüsten": null,
- "Nehren": null,
- "Neidenfels": null,
- "Neidenstein": null,
- "Neinstedt": null,
- "Nejdek": null,
- "Nejdek zastavka": null,
- "Nejdek-Oldrichov": null,
- "Nejdek-Sejfy": null,
- "Nejdek-Sucha": null,
- "Nejdek-Tisova": null,
- "Nellmersbach": null,
- "Nemmenich": null,
- "Nemours St Pierre": null,
- "Nemsdorf-Göhrendorf": null,
- "Nendeln": null,
- "Nendingen(b Tuttlingen)": null,
- "Nennhausen": null,
- "Nennig": null,
- "Nennigmühle": null,
- "Nenzing": null,
- "Nenzingen": null,
- "Nersingen": null,
- "Nesselwang": null,
- "Nessonvaux": null,
- "Nestedice": null,
- "Nestemice": null,
- "Nettersheim": null,
- "Nettingsdorf": null,
- "Netzeband": null,
- "Netzkater": null,
- "Netzschkau": null,
- "Neu Pudagla": null,
- "Neu St Jürgen": null,
- "Neu Wokern": null,
- "Neu Wulmstorf": null,
- "Neu-Anspach": null,
- "Neu-Edingen/Friedrichsfeld": null,
- "Neu-Isenburg": null,
- "Neu-Ulm": null,
- "Neubiberg": null,
- "Neubrandenburg": null,
- "Neubrücke(Nahe)": null,
- "Neubukow": null,
- "Neuburg(Donau)": null,
- "Neuburg(Kammel)": null,
- "Neuburg(Rhein)": null,
- "Neubäu": null,
- "Neuchâtel": null,
- "Neudenau": null,
- "Neudietendorf": null,
- "Neudorf(Erzgeb)": null,
- "Neue Schenke": null,
- "Neuenburg(Baden)": null,
- "Neuenbürg(Enz)": null,
- "Neuenbürg(Enz) Freibad": null,
- "Neuenbürg(Enz) Süd": null,
- "Neuenbürg(Enz)-Rotenbach Eyachbrücke": null,
- "Neuendettelsau": null,
- "Neuenhagen(b Berlin)": null,
- "Neuenhaus": null,
- "Neuenhaus Süd": null,
- "Neuenkirchen(Oldb)": null,
- "Neuenmarkt-Wirsberg": null,
- "Neuenrade": null,
- "Neuenstein": null,
- "Neufahrn(Niederbay)": null,
- "Neufahrn(b Freising)": null,
- "Neufchateau(B)": null,
- "Neufchateau(F)": 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(CH)": null,
- "Neuhausen(Cottbus)": null,
- "Neuhof(Kr Fulda)": null,
- "Neuhof(b Zossen)": null,
- "Neukieritzsch": null,
- "Neukirch(Lausitz)Ost": null,
- "Neukirch(Lausitz)West": null,
- "Neukirch-Egnach": 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-Kallham": null,
- "Neumarkt-St Veit": null,
- "Neumarkt/Wallersee": 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,
- "Neuratting": null,
- "Neureut Adolf-Ehrmann-Bad, Karlsruhe": null,
- "Neureut Bärenweg, Karlsruhe": null,
- "Neureut Welschneureuter Straße, Karlsruhe": null,
- "Neuruppin Rheinsberger Tor": null,
- "Neuruppin West": null,
- "Neusalza-Spremberg": null,
- "Neuses(b Kronach)": null,
- "Neusorg": null,
- "Neuss Allerheiligen": null,
- "Neuss Am Kaiser": 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,
- "Neuwiesenreben, Ettlingen": null,
- "Neuwirtshaus(Porscheplatz)": null,
- "Neuzelle": null,
- "Neuötting": null,
- "Nice Ville": null,
- "Nidda": null,
- "Nidderau": null,
- "Nidderau-Eichen": null,
- "Nidderau-Windecken": null,
- "Nideggen-Brück": null,
- "Niebüll": null,
- "Niebüll Autoverladung": null,
- "Niebüll neg": null,
- "Niedaltdorf": null,
- "Nieder Flörsheim-Dalsheim": null,
- "Nieder Olm": null,
- "Nieder Wöllstadt": null,
- "Nieder-Ohmen": null,
- "Niederarnbach": null,
- "Niederau": null,
- "Niederau-Tuchmühle": null,
- "Niederbiegen": null,
- "Niederbipp": null,
- "Niederbobritzsch": null,
- "Niederbrechen": null,
- "Niederdollendorf": null,
- "Niederdorf(Erzgeb)": null,
- "Niederdorfelden": null,
- "Niederdreisbach": null,
- "Niederdresselndorf": null,
- "Niedererbach": null,
- "Niederfinow": null,
- "Niederglatt": null,
- "Niedergörsdorf": null,
- "Niederhadamar": null,
- "Niederheimbach": null,
- "Niederhöchstadt": null,
- "Niederhövels": null,
- "Niederjosbach": null,
- "Niederkorn": 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,
- "Niesky": null,
- "Nieukerk": null,
- "Nieuw Amsterdam": null,
- "Nieuw Vennep": null,
- "Nieuwerkerk a. d. Ijssel": null,
- "Nievenheim": null,
- "Nievern": null,
- "Nijkerk": null,
- "Nijmegen": null,
- "Nijmegen Dukenburg": null,
- "Nijmegen Goffert": null,
- "Nijmegen Heyendaal": null,
- "Nijmegen Lent": null,
- "Nijverdal": null,
- "Niklashausen": null,
- "Nimburg(Baden)": null,
- "Nistertal-Bad Marienberg": null,
- "Nittel": null,
- "Noerre Nebel st": null,
- "Noerreport st": null,
- "Noertzange": null,
- "Nogent-le-Rotrou": null,
- "Nohen": null,
- "Nohfelden": null,
- "Nohra(Weimar)": null,
- "Nohra(Wipper)": null,
- "Noisy-le-Sec": null,
- "Nonnenhorn": 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,
- "Nordhorn": null,
- "Nordhorn-Blanke": null,
- "Nordsode": null,
- "Nordstemmen": null,
- "Nordwalde": null,
- "Norf": null,
- "Norheim": null,
- "Norrköping Central": null,
- "Norsingen": null,
- "Northeim(Han)": null,
- "Nortorf": null,
- "Nossentin": null,
- "Notre-Dame-de-Briancon": null,
- "Nottuln-Appelhülsen": null,
- "Nova Gradiska": null,
- "Nova Kapela": null,
- "Nova Role": null,
- "Nova Role zastavka": null,
- "Novara": null,
- "Nove Hamry": null,
- "Nove Zamky": null,
- "Noveant": null,
- "Novska": null,
- "Novy Bor": null,
- "Noyon": null,
- "Nufringen": null,
- "Nunspeet": null,
- "Nuth": null,
- "Nußberg-Schönau": null,
- "Ny Ellebjerg st": null,
- "Nyborg st": null,
- "Nyiregyhaza": null,
- "Nymburk hl.n.": null,
- "Nässjö Central": null,
- "Nîmes": null,
- "Nöbdenitz": null,
- "Nördlingen": null,
- "Nörten-Hardenberg": null,
- "Nörvenich-Binsfeld": null,
- "Nünchritz": null,
- "Nürnberg Frankenstadion": 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-Sandreuth": null,
- "Nürnberg-Schweinau": null,
- "Nürnberg-Stein": null,
- "Nürnberg-Steinbühl": null,
- "Nürtingen": null,
- "Nürtingen-Roßdorf": null,
- "Nürtingen-Vorstadt": null,
- "Nützen": null,
- "Nüziders": null,
- "Ober Ramstadt": null,
- "Ober Widdersheim": null,
- "Oberachern": null,
- "Oberachern Bindfadenfabrik": null,
- "Oberaichen": null,
- "Oberalm": null,
- "Oberammergau": null,
- "Oberasbach": null,
- "Oberau": null,
- "Oberaudorf": null,
- "Oberbettingen-Hillesheim": null,
- "Oberbillig": null,
- "Oberbimbach": null,
- "Oberboihingen": null,
- "Oberbrechen": null,
- "Oberburg": null,
- "Obercarsdorf": null,
- "Oberdachstetten": null,
- "Oberderdingen-Flehingen Industrie": null,
- "Oberelchingen": null,
- "Oberelsungen": null,
- "Obererbach": null,
- "Oberesslingen": null,
- "Oberferrieden": null,
- "Obergimpern": null,
- "Oberglatt": 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-Sterkrade": null,
- "Oberhofen im Inntal": null,
- "Oberholz": null,
- "Oberkirch": null,
- "Oberkirch-Köhlersiedlung": null,
- "Oberkochen": null,
- "Oberkorn": null,
- "Oberkotzau": null,
- "Oberkrozingen": null,
- "Oberlahnstein": null,
- "Oberlauscha": null,
- "Oberlenningen": null,
- "Oberlichtenau": null,
- "Oberlindhart": null,
- "Oberlinxweiler": null,
- "Obermaubach": null,
- "Obermodern": null,
- "Obermohr": null,
- "Obernau": null,
- "Obernberg-Altheim": null,
- "Obernburg-Elsenfeld": null,
- "Oberndorf(Neckar)": null,
- "Oberndorf(Wittgenstein)": null,
- "Oberneuschönberg": null,
- "Obernhof(Lahn)": null,
- "Oberoderwitz": null,
- "Oberoderwitz Oberdorf": null,
- "Oberottmarshausen": null,
- "Oberrieden(CH)": null,
- "Oberriet": 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,
- "Oberursel-Weiß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,
- "Obourg": null,
- "Obstfelderschmiede": null,
- "Ochenbruck": null,
- "Ochsenfurt": null,
- "Ochsenhausen": null,
- "Ochtmersleben": null,
- "Ochtrup": null,
- "Ockenheim": null,
- "Odenheim Bf": null,
- "Odenheim West": null,
- "Odense st": 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,
- "Oerestad st": null,
- "Oerlenbach": null,
- "Oerlinghausen": null,
- "Oermingen": null,
- "Oertzenhof": null,
- "Oese": null,
- "Oesede": null,
- "Oesterport st": null,
- "Oestrich-Winkel": null,
- "Oetrange": 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,
- "Oisterwijk": null,
- "Okarben": null,
- "Oker": null,
- "Oksboel st": 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,
- "Oldenzaal": null,
- "Olen(Belgien)": null,
- "Olovi": null,
- "Olpe": null,
- "Olsberg": null,
- "Olsbrücken": null,
- "Olst(NL)": null,
- "Olten": null,
- "Ommen": null,
- "Onville": null,
- "Oostende": null,
- "Oosterbeek": null,
- "Opfikon": null,
- "Opheusden": null,
- "Opladen": null,
- "Opole Glowne": null,
- "Oppenau": null,
- "Oppenheim": null,
- "Oppenweiler(Württ)": null,
- "Oppikon": null,
- "Oppurg": null,
- "Opwijk": null,
- "Orange(Avignon)": null,
- "Oranienbaum(Anh)": null,
- "Oranienburg": null,
- "Oranienburg (S)": null,
- "Orchies": null,
- "Orlamünde": null,
- "Orléans": null,
- "Orschweier": null,
- "Ortrand": null,
- "Oschatz": null,
- "Oschersleben(Bode)": null,
- "Osnabrück Altstadt": null,
- "Osnabrück Hbf": null,
- "Osnabrück-Sutthausen": null,
- "Oss": null,
- "Oss West": null,
- "Ostbevern": null,
- "Ostendstraße, Karlsruhe": null,
- "Osterburg": null,
- "Osterburken": null,
- "Osterhofen(Nby)": null,
- "Osterhofen(Oberbay)": null,
- "Osterholz-Scharmbeck": null,
- "Ostermundigen": 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(Kr Hanau)": null,
- "Ostheim(b Butzbach)": null,
- "Osthofen": null,
- "Ostrach Bahnhof": null,
- "Ostrau": null,
- "Ostrava hl.n.": null,
- "Ostrava-Svinov": null,
- "Ostseebad Binz": null,
- "Ostseebad Kühlungsborn Mitte": null,
- "Ostseebad Kühlungsborn Ost": null,
- "Ostseebad Kühlungsborn West": null,
- "Othmarsingen": null,
- "Otrokovice": 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,
- "Otto-Sachs-Straße, Karlsruhe": null,
- "Ottobeuren": null,
- "Ottobrunn": null,
- "Ottweiler(Saar)": null,
- "Otzberg Lengfeld": null,
- "Otze": null,
- "Otzing": null,
- "Oudenbosch": null,
- "Outrup st": null,
- "Ovelgünne": null,
- "Overath": null,
- "Overveen": null,
- "Owen(Teck)": null,
- "Owschlag": null,
- "Oy-Mittelberg": null,
- "Oßmannstedt": null,
- "Padborg st": null,
- "Paderborn Hbf": null,
- "Paderborn Kasseler Tor": null,
- "Paderborn Nord": null,
- "Paderborn-Schloss Neuhaus": null,
- "Paderborn-Sennelager": null,
- "Padova": null,
- "Paffendorf": null,
- "Pagny-sur-Moselle": null,
- "Paindorf": null,
- "Palzem": null,
- "Pankofen": null,
- "Pansdorf": null,
- "Pantin": null,
- "Papenburg(Ems)": null,
- "Papendorf": null,
- "Papierfabrik, Kaufungen": null,
- "Papiermühle(Stadtr)": null,
- "Pappenheim": null,
- "Parchim": null,
- "Pardubice hl.n.": null,
- "Paris Austerlitz": null,
- "Paris Est": null,
- "Paris Gare de Lyon": null,
- "Paris Montparnasse": null,
- "Paris Nord": null,
- "Paris St Lazare": null,
- "Parkentin": null,
- "Parndorf": null,
- "Parsberg": null,
- "Partenstein": null,
- "Pasewalk": null,
- "Pasewalk Ost": null,
- "Passau Hbf": null,
- "Passow(Uckermark)": null,
- "Paternion-Feistritz": null,
- "Patersdorf": null,
- "Patsch": null,
- "Pau": null,
- "Paulinenaue": null,
- "Paulinzella": null,
- "Pavia": null,
- "Pechbrunn": null,
- "Peenemünde": null,
- "Pegau": null,
- "Peggau-Deutschfeistritz": 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,
- "Peltre": null,
- "Penig": null,
- "Penzberg": null,
- "Pepinster": null,
- "Perigueux": null,
- "Perkam": null,
- "Perl": null,
- "Perleberg": null,
- "Pernink": null,
- "Perpignan": null,
- "Peschiera del Garda": null,
- "Petange": null,
- "Petergrube": null,
- "Petersaurach": null,
- "Petersaurach Nord": null,
- "Petershagen Nord": null,
- "Petershagen(Uckerm)": null,
- "Petershagen-Lahde": null,
- "Petershain": null,
- "Petershausen(Obb)": null,
- "Peterskirchen": null,
- "Petersroda": null,
- "Petit Croix": null,
- "Pfaffenhain": null,
- "Pfaffenhausen": null,
- "Pfaffenhofen(Ilm)": null,
- "Pfalzel": null,
- "Pfarrkirchen": null,
- "Pfarrwerfen": null,
- "Pflach": null,
- "Pflaumloch": null,
- "Pforzheim Hbf": null,
- "Pforzheim Maihälden": null,
- "Pforzheim-Weißenstein": null,
- "Pfraundorf(Inn)": null,
- "Pfreimd": null,
- "Pfronten-Ried": null,
- "Pfronten-Steinach": null,
- "Pfronten-Weißbach": null,
- "Pfullendorf": null,
- "Pfungstadt": null,
- "Pfäffikon SZ": null,
- "Pfäffingen": null,
- "Philipp-Reis-Straße, Karlsruhe": null,
- "Philippsburg(Baden)": null,
- "Philippshagen": null,
- "Philippsheim": null,
- "Philippstraße, Karlsruhe": null,
- "Pichl b.Schladming": null,
- "Piding": null,
- "Piensk": null,
- "Pill-Vomperbach": null,
- "Pillgram": null,
- "Pinneberg": null,
- "Pinnow(Uckermark)": null,
- "Pino transito": null,
- "Pinzberg": null,
- "Pirk": null,
- "Pirmasens Hbf": null,
- "Pirmasens Nord": null,
- "Pirna": null,
- "Pirna-Copitz": null,
- "Pirna-Copitz Nord": null,
- "Pisa Centrale": null,
- "Plaaz": null,
- "Plaidt": null,
- "Planegg": null,
- "Plate(Meckl)": null,
- "Plattling": null,
- "Platz der Deutschen Einheit, Kassel": null,
- "Plau am See Bahnhof": null,
- "Plaue(Thür)": null,
- "Plauen(V) unt Bf": null,
- "Plauen(Vogtl) Mitte": null,
- "Plauen(Vogtl) ob Bf": null,
- "Plauen(Vogtl)-Straßberg": null,
- "Plauen(Vogtl)West": null,
- "Pleinfeld": null,
- "Plesna(CZ)": null,
- "Plessa": null,
- "Plettenberg": null,
- "Plochingen": null,
- "Ploiesti Vest": null,
- "Plzen hl.n.": null,
- "Plön": null,
- "Plüderhausen": null,
- "Plüschow": null,
- "Pockau-Lengefeld": null,
- "Pocking": null,
- "Pogeez": null,
- "Poggenhagen": null,
- "Poikam": null,
- "Poing": null,
- "Poitiers": null,
- "Pomezi nad Ohri": null,
- "Pommelsbrunn": null,
- "Pommern(Mosel)": null,
- "Pommritz": null,
- "Ponitz": null,
- "Pont St Vincent": null,
- "Pont-Ste-Maxence": null,
- "Pont-a-Mousson": null,
- "Ponte Gardena-Laion/Waidbruck-Lajen": null,
- "Pontresina": null,
- "Poppenhausen": null,
- "Pordenone": null,
- "Porschdorf(Pirna)": null,
- "Porstendorf": null,
- "Port Bou": null,
- "Port Vendres Ville": null,
- "Porta Westfalica": null,
- "Porz(Rhein)": null,
- "Porz-Wahn": null,
- "Posewald": null,
- "Possenhofen": null,
- "Postbauer-Heng": null,
- "Poststraße, Karlsruhe": null,
- "Potsdam Charlottenhof": null,
- "Potsdam Griebnitzsee": null,
- "Potsdam Griebnitzsee (S)": null,
- "Potsdam Hbf": null,
- "Potsdam Hbf (S)": null,
- "Potsdam Medienstadt Babelsberg": null,
- "Potsdam Park Sanssouci": null,
- "Potsdam Pirschheide": null,
- "Potsdam-Babelsberg": null,
- "Potsdam-Rehbrücke": null,
- "Potucky": null,
- "Potucky zastavka": null,
- "Pougues les Eaux": null,
- "Poznan Gl.": null,
- "Praest": null,
- "Praha hl.n.": null,
- "Praha-Holesovice": null,
- "Praha-Smichov": null,
- "Pram-Haag": null,
- "Pratau": null,
- "Pratteln": null,
- "Predeal": null,
- "Preetz": null,
- "Pregarten": null,
- "Premnitz Nord": null,
- "Premnitz Zentrum": null,
- "Prenzlau": null,
- "Prerov": null,
- "Pressath": null,
- "Pressig-Rothenkirchen": null,
- "Pretzfeld": null,
- "Pretzier(Altm)": null,
- "Pretzsch": null,
- "Priemerburg": null,
- "Prien a Chiemsee": null,
- "Priestewitz": null,
- "Prinzersdorf": null,
- "Priort": null,
- "Prisdorf": null,
- "Prittitz": null,
- "Pritzerbe": null,
- "Pritzier": null,
- "Pritzwalk": null,
- "Pritzwalk Hainholz": null,
- "Pritzwalk West": null,
- "Probstzella": null,
- "Profen": null,
- "Profondsart": null,
- "Prora": null,
- "Prora Ost": null,
- "Prosselsheim": null,
- "Przylep": null,
- "Prödel": null,
- "Prösen": null,
- "Prösen Ost": null,
- "Prösen West": null,
- "Puch bei Hallein": null,
- "Puchheim": null,
- "Pulheim": null,
- "Pullach": null,
- "Pulling(b Freising)": null,
- "Pulsnitz": null,
- "Pulsnitz Süd": null,
- "Purmerend": null,
- "Purmerend Overwhere": null,
- "Purmerend Weidevenne": null,
- "Pusarnitz": null,
- "Puschendorf": null,
- "Putbus": null,
- "Putten": null,
- "Puttgarden": null,
- "Putzkau": null,
- "Pöchlarn": null,
- "Pölchow": null,
- "Pölling": null,
- "Pöllwitz": null,
- "Pönitz(Holst)": null,
- "Pönitz(Leipzig)": null,
- "Pörtschach am Wörther See": 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-Kupferheide": null,
- "Quendorf": null,
- "Querfurt": null,
- "Quevy": null,
- "Quickborn": null,
- "Quickborn Süd": null,
- "Quickborner Straße": null,
- "Quierschied": null,
- "Quimper": null,
- "Quint": null,
- "Raaba": null,
- "Raalte": null,
- "Rabenau": null,
- "Rackith(Elbe)": null,
- "Rackwitz(Leipzig)": null,
- "Radbruch": null,
- "Raddusch": null,
- "Radeberg": 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,
- "Radstadt": null,
- "Rafz": null,
- "Raguhn": null,
- "Rahden": null,
- "Rain": null,
- "Raindorf": 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,
- "Rankweil": null,
- "Ranstadt": null,
- "Ranzo-S. Abbondio": null,
- "Rastatt": null,
- "Rastatt Beinle": null,
- "Rastede": null,
- "Rastow": null,
- "Rathaus, Kassel": null,
- "Rathaus/Fünffensterstraße, Kassel": null,
- "Rathenow": null,
- "Rathmannsdorf(Kr Pirna)": null,
- "Ratingen Ost": null,
- "Rattenberg-Kramsach": null,
- "Ratzeburg": null,
- "Raubling": null,
- "Rauenstein(Thür)": null,
- "Raumland-Markhausen": null,
- "Raumünzach": null,
- "Raun": null,
- "Raunheim": null,
- "Ravensburg": null,
- "Ravenstein": null,
- "Re(I)": null,
- "Rebdorf-Hofmühle": null,
- "Rebstein-Marbach": null,
- "Rech": null,
- "Rechenberg": null,
- "Rechenberg Schule": 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,
- "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 Kurpark, Waldbronn": null,
- "Reichenbach im Kandertal": null,
- "Reichenbach(Fils)": null,
- "Reichenbach(Oberlausitz)": null,
- "Reichenbach(Vogtl) ob Bf": null,
- "Reichenbach(b. Ettlingen)": null,
- "Reichenberg(Unterfr)": null,
- "Reichenburg": null,
- "Reichenschwand": null,
- "Reichersbeuern": null,
- "Reichertshausen(Ilm)": null,
- "Reichertshofen(Schwab) Bf": null,
- "Reicholzheim": null,
- "Reifland-Wünschendorf": null,
- "Reihen": null,
- "Reil": null,
- "Reilsheim": null,
- "Reims": 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,
- "Reith b.Seefeld": null,
- "Rejsby st": null,
- "Rekawinkel": null,
- "Reken": null,
- "Reken-Klein Reken": null,
- "Rekingen AG": null,
- "Remagen": null,
- "Remiremont": null,
- "Remscheid Hbf": null,
- "Remscheid-Güldenwerth": null,
- "Remscheid-Lennep": null,
- "Remscheid-Lüttringhausen": null,
- "Renchen": null,
- "Rendsburg": null,
- "Rennes": null,
- "Renningen": null,
- "Renningen Süd": null,
- "Rennsteig": null,
- "Rentrisch": null,
- "Rentweinsdorf": null,
- "Rentwertshausen": null,
- "Rentzschmühle": null,
- "Retenice": null,
- "Rethen(Leine)": null,
- "Retz": 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,
- "Reutte in Tirol": null,
- "Reutte in Tirol Schulzentrum": null,
- "Reuver": null,
- "Reußen": null,
- "Rhade": null,
- "Rheda-Wiedenbrück": null,
- "Rheden(NL)": null,
- "Rheinbach": null,
- "Rheinbach Römerkanal": null,
- "Rheinberg(Rheinl)": null,
- "Rheinbrohl": null,
- "Rheine": null,
- "Rheine-Mesum": null,
- "Rheineck": null,
- "Rheinfelden(Baden)": null,
- "Rheinfelden(CH)": null,
- "Rheinhafen, Karlsruhe": null,
- "Rheinhafenstraße, Karlsruhe": 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,
- "Rhenen": null,
- "Rhens": null,
- "Rheydt Hbf": null,
- "Rheydt-Odenkirchen": null,
- "Rhöndorf": null,
- "Ribe Noerremark st": null,
- "Ribe st": null,
- "Ribnitz-Damgarten Ost": null,
- "Ribnitz-Damgarten West": null,
- "Richen(b Eppingen)": null,
- "Richterswil": null,
- "Rickling": null,
- "Ried": null,
- "Ried im Innkreis": null,
- "Riederau": null,
- "Riedlingen": null,
- "Riedrode": null,
- "Riedstadt-Goddelau": null,
- "Riedstadt-Wolfskehlen": null,
- "Riegel am Kaiserstuhl Ort": null,
- "Riegel-Malterdingen": null,
- "Riegel-Malterdingen NE": null,
- "Riehen": null,
- "Riehen Niederholz": null,
- "Rieneck": null,
- "Riesa": null,
- "Rieschweiler": null,
- "Rieseby": null,
- "Rieste": null,
- "Riestedt": null,
- "Rietheim(CH)": null,
- "Rietheim(Württ)": null,
- "Rietschen": null,
- "Rietz in Tirol": null,
- "Riffelriß, Grainau": null,
- "Rijssen": null,
- "Rijswijk": null,
- "Rilland-Bath": null,
- "Rimbach": null,
- "Rimini": null,
- "Ringenwalde(Templin)": null,
- "Ringleben-Gebesee": null,
- "Ringsheim": null,
- "Ringsted st": null,
- "Rinkerode": null,
- "Rinklingen": null,
- "Rinnthal": null,
- "Rinteln": null,
- "Rintheim Sinsheimer Straße, Karlsruhe": null,
- "Rio di Pusteria/Mühlbach": null,
- "Rippberg": null,
- "Ritschenhausen": null,
- "Ritterhude": null,
- "Rivera-Bironico": null,
- "Rivesaltes": null,
- "Rixheim(Mulhouse)": null,
- "Roanne": null,
- "Robilante": null,
- "Roccavione": null,
- "Rochefort-Jemelle": null,
- "Rochlitz(Sachs)": null,
- "Rockenhausen": null,
- "Rodalben": null,
- "Rodange": 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,
- "Roedekro st": null,
- "Roermond": null,
- "Roeschwoog": null,
- "Roggentin": null,
- "Roggwil-Berg": null,
- "Roggwil-Wynau": null,
- "Rohr(Thür)": null,
- "Rohr-Bad Hall": null,
- "Rohrbach(Ilm)": null,
- "Rohrbach(Oberbay)": null,
- "Rohrbach(Pfalz)": null,
- "Rohrbach(Saar)": null,
- "Rohrdorf(Oberbay)": null,
- "Rohrenfeld": null,
- "Roigheim": null,
- "Roisdorf": null,
- "Roitzsch(Bitterf)": null,
- "Rokycany": null,
- "Rolandseck": null,
- "Rollhofen": null,
- "Roma Termini": null,
- "Romanshorn": null,
- "Romanshorn (See)": null,
- "Rombas-Clouange": null,
- "Rommelshausen": null,
- "Rommerskirchen": null,
- "Ronet": null,
- "Ronneburg(Thür)": null,
- "Ronnenberg": null,
- "Ronshausen": null,
- "Roodeschool": null,
- "Roodt/Syre": null,
- "Roosendaal": null,
- "Roppen": null,
- "Rorschach": null,
- "Rorschach Hafen": null,
- "Rorschach Hafen (See)": null,
- "Rosbach v d Höhe": null,
- "Rosbach(Sieg)": null,
- "Rosenau(b Grafenau)": null,
- "Rosenbach bei Villach": null,
- "Rosenberg(Baden)": null,
- "Rosendahl-Holtwick": null,
- "Rosenheim": null,
- "Rosenheim Aicherpark": null,
- "Rosenheim Hochschule": null,
- "Rosenwinkel": null,
- "Roskilde st": null,
- "Rosmalen": null,
- "Rostock Hbf": null,
- "Rostock Holbeinplatz": null,
- "Rostock Parkstraße": null,
- "Rostock Seehafen Nord": 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,
- "Rotava": null,
- "Rotenbach(Enz)": null,
- "Rotenburg a.d. Fulda": null,
- "Rotenburg(Wümme)": null,
- "Rotenhain": null,
- "Roth": null,
- "Rothenburg ob der Tauber": null,
- "Rothenburg(CH)": null,
- "Rothenbürg": null,
- "Rothenstein(Saale)": null,
- "Rothenthurm(CH)": null,
- "Rothrist": null,
- "Rotkreuz": null,
- "Rott(Inn)": null,
- "Rottenacker": null,
- "Rottenbach": null,
- "Rottenburg(Neckar)": null,
- "Rottendorf": null,
- "Rotterdam Alexander": null,
- "Rotterdam Blaak": null,
- "Rotterdam Centraal": null,
- "Rotterdam Lombardijen": null,
- "Rotterdam Noord": null,
- "Rotterdam Zuid": null,
- "Rottershausen": null,
- "Rottweil": null,
- "Rottweil Göllsdorf": null,
- "Rottweil Neufra": null,
- "Rottweil Saline": null,
- "Roudnice nad Labem": null,
- "Rouffach": null,
- "Rovereto": null,
- "Rovigo": 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,
- "Rudolstadt(Thür)": null,
- "Rudolstadt-Schwarza": null,
- "Ruhland": null,
- "Ruhlsdorf-Zerpenschleuse": null,
- "Ruhmannsfelden": null,
- "Ruhpolding": null,
- "Ruhstorf": null,
- "Rum b.Innsbruck": null,
- "Rumburk": null,
- "Rumeln": null,
- "Rummenohl": null,
- "Runding": null,
- "Runkel": null,
- "Rupperswil": null,
- "Ruppertsgrün": null,
- "Rupprechtstegen": null,
- "Ruschberg": null,
- "Ruschwedel": null,
- "Rutesheim": null,
- "Ruthenbeck": null,
- "Ruurlo": null,
- "Rybniste": null,
- "Rzepin": null,
- "Rätzlingen": null,
- "Réding(F)": null,
- "Rémilly": 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ümikon AG": null,
- "Rümlang": null,
- "Rümmingen": null,
- "Ründeroth": null,
- "Rüppurr Battstraße, Karlsruhe": null,
- "Rüppurr Ostendorfplatz, Karlsruhe": null,
- "Rüppurr Tulpenstraße, Karlsruhe": null,
- "Rüppurrer Tor, Karlsruhe": null,
- "Rüschlikon": null,
- "Rüsselbach": null,
- "Rüsselsheim": null,
- "Rüsselsheim Opelwerk": null,
- "Rüthi SG": null,
- "Saal(Donau)": null,
- "Saalburg(Taunus)": null,
- "Saalfeld(Saale)": null,
- "Saalfelden": 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,
- "Safenwil": null,
- "Sagard": null,
- "Sagehorn": null,
- "Saincaize": null,
- "Saint Ghislain": null,
- "Saintes": null,
- "Salach": null,
- "Salem": null,
- "Salez-Sennwald": null,
- "Sallach": null,
- "Salmtal": null,
- "Salzbergen": null,
- "Salzburg Aigen": null,
- "Salzburg Aiglhof": null,
- "Salzburg Gnigl": null,
- "Salzburg Hbf": null,
- "Salzburg Liefering": null,
- "Salzburg Mülln-Altstadt": null,
- "Salzburg Parsch": null,
- "Salzburg Sam": null,
- "Salzburg Süd": null,
- "Salzburg Taxham Europark": null,
- "Salzgitter-Bad": null,
- "Salzgitter-Immendorf": null,
- "Salzgitter-Lebenstedt": null,
- "Salzgitter-Ringelheim": null,
- "Salzgitter-Thiede": null,
- "Salzgitter-Watenstedt": null,
- "Salzkotten": null,
- "Salzwedel": null,
- "Samedan": null,
- "Samstagern": null,
- "Samtens": null,
- "San Candido/Innichen": null,
- "Sand(Niederbay)": null,
- "Sande": null,
- "Sandebeck": null,
- "Sanderbusch": null,
- "Sandersdorf(Bitterf)": null,
- "Sandershäuser Straße, Kassel": null,
- "Sandersleben(Anh)": null,
- "Sandförde": null,
- "Sandhagen(b Bad Dob)": null,
- "Sandkrug": null,
- "Sandwehle": null,
- "Sandweiler-Contern": null,
- "Sangerhausen": null,
- "Sanitz(b Rostock)": null,
- "Sankt Augustin Zentrum": null,
- "Sanry-sur-Nied": null,
- "Sanssouci": null,
- "Santpoort Noord": null,
- "Santpoort Zuid": null,
- "Sapjane(Gr)": null,
- "Sargans": null,
- "Sarnow": null,
- "Sarrebourg": null,
- "Sarreguemines": null,
- "Sarstedt": null,
- "Sasbach am Kaiserstuhl": null,
- "Sassenheim": null,
- "Sassenroth": null,
- "Sassnitz": null,
- "Sathonay Rillieux": null,
- "Satteldorf": null,
- "Satzvey": null,
- "Sauerlach": null,
- "Sauldorf": null,
- "Saulgrub": null,
- "Saulheim": null,
- "Saumur Rive Droit": null,
- "Sauwerd": null,
- "Saverne": null,
- "Schaan-Vaduz": null,
- "Schaerbeek": null,
- "Schafbrücke": null,
- "Schaffhausen": null,
- "Schaftenau": null,
- "Schaftlach": null,
- "Schagen": 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,
- "Scharnitz": null,
- "Scharstorf": null,
- "Schechen": null,
- "Scheemda": null,
- "Scheeßel": null,
- "Scheibenberg": null,
- "Scheidemannplatz, Kassel": null,
- "Scheidt(Saar)": null,
- "Schelklingen": null,
- "Schemmerberg": null,
- "Schenkenzell": null,
- "Scheppach": null,
- "Scherfede": null,
- "Scheuerfeld(Sieg)": null,
- "Scheven": null,
- "Schiedam Centrum": null,
- "Schieder": null,
- "Schierbrok": null,
- "Schierke": null,
- "Schierstedt": null,
- "Schifferstadt": null,
- "Schifferstadt Süd": null,
- "Schifflange": null,
- "Schiffweiler": null,
- "Schillerstraße, Karlsruhe": null,
- "Schiltach": null,
- "Schiltach Mitte": null,
- "Schimborn": null,
- "Schin op Geul": null,
- "Schindellegi-Feusisberg": null,
- "Schinnen": null,
- "Schiphol (Airport)": null,
- "Schirgiswalde-Kirschau": null,
- "Schirnding": null,
- "Schkeuditz": null,
- "Schkeuditz West": null,
- "Schkopau": null,
- "Schladen(Harz)": null,
- "Schladern(Sieg)": null,
- "Schladming": null,
- "Schlatt(Hohenz)": null,
- "Schlechtbach": null,
- "Schleife": null,
- "Schleswig": null,
- "Schliengen": null,
- "Schlierbach(Schwalm-Eder-Kr.)": null,
- "Schliersee": null,
- "Schlins-Beschling": null,
- "Schloss Gottesaue, Karlsruhe": null,
- "Schloss Rüppurr, Karlsruhe": 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,
- "Schmiedeberg (Dresden)": null,
- "Schmiedeberg-Naundorf": 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,
- "Schopfheim-Schlattholz": null,
- "Schopfloch(b Freudenstadt)": null,
- "Schopp": null,
- "Schorndorf": null,
- "Schorndorf-Hammerschlag": null,
- "Schortens-Heidmühle": null,
- "Schouweiler": null,
- "Schrezheim": null,
- "Schrobenhausen": null,
- "Schrozberg": null,
- "Schruns": null,
- "Schulen": 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,
- "Schwarzach i Vorarl.": null,
- "Schwarzach-St.Veit": 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,
- "Schwaz": null,
- "Schwechat": 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-Lankow": null,
- "Schwerin-Margaretenhof": null,
- "Schwerin-Warnitz": null,
- "Schwerin-Wüstmark": null,
- "Schwerte(Ruhr)": null,
- "Schweta Bf": null,
- "Schwetzingen": null,
- "Schwieberdingen": null,
- "Schwindebeck": null,
- "Schwindegg": null,
- "Schwindratzheim": null,
- "Schwyz": null,
- "Schwäbisch Gmünd": null,
- "Schwäbisch Hall": null,
- "Schwäbisch Hall-Hessental": null,
- "Schwörstadt": null,
- "Schärding": 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önbichl in Tirol": 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önmünzach": null,
- "Schönow(Angerm)": null,
- "Schönstedt": null,
- "Schönwald(Oberfr)": null,
- "Schönwalde(Barnim)": null,
- "Schönwalde(Spreewald)": null,
- "Schönwies": null,
- "Schöppenstedt": null,
- "Schötmar": null,
- "Schübelbach-Buttikon": null,
- "Schülldorf": null,
- "Schüptitz": null,
- "Schüttorf": null,
- "Scuol-Tarasp": null,
- "Sebnitz(Sachs)": null,
- "Sebuzin": null,
- "Sechshelden": null,
- "Sechtem": null,
- "Seckach": null,
- "Seddin": null,
- "Sedlitz Ost": null,
- "Seebach(Mühlhausen)": null,
- "Seebad Ahlbeck": null,
- "Seebad Heringsdorf": null,
- "Seebad Lubmin": null,
- "Seebergen": null,
- "Seebrugg": null,
- "Seefeld in Tirol": null,
- "Seefeld(Mark)": null,
- "Seefeld-Hechendorf": null,
- "Seeg": null,
- "Seegefeld": null,
- "Seehausen(Altm)": null,
- "Seehausen(Uckermark)": null,
- "Seekirchen am Wallersee": 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,
- "Sejstrup st": 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,
- "Selzthal": null,
- "Semmering": null,
- "Senden": null,
- "Senden-Bösensell": null,
- "Senftenberg": null,
- "Sennfeld": null,
- "Serams": null,
- "Seregno": null,
- "Serrig": null,
- "Sersheim": null,
- "Sesto S. Giovanni": null,
- "Sete": null,
- "Seubersdorf": null,
- "Seulberg": null,
- "Seulbitz": null,
- "Sevelen": null,
- "Sevnica": null,
- "Seybothenreuth": null,
- "Siebeldingen-Birkweiler": null,
- "Siebnen-Wangen": null,
- "Siedenlangenbeck": null,
- "Siedlinghausen": null,
- "Siegburg Bahnhof": null,
- "Siegburg/Bonn": null,
- "Siegelsbach": null,
- "Siegelsdorf": null,
- "Siegen Hbf": null,
- "Siegen-Geisweid": null,
- "Siegen-Weidenau": null,
- "Siegershausen": null,
- "Siegsdorf": null,
- "Sieniawa Zarska": null,
- "Sierck-les-Bains": null,
- "Sierentz": null,
- "Sierksdorf": null,
- "Sierre/Siders": null,
- "Siersburg": null,
- "Siershahn": null,
- "Sieversdorf(Neust/D)": null,
- "Siggenthal-Würenlingen": null,
- "Sighisoara": null,
- "Siglingen": null,
- "Sigmaringen": null,
- "Sigmaringendorf": null,
- "Silbach": null,
- "Silberhausen": null,
- "Silberhütte NE": null,
- "Silberstraße": null,
- "Sillian": null,
- "Silz im Oberinntal": null,
- "Simbach(Inn)": null,
- "Simeria": null,
- "Simmelsdorf-Hüttenbach": null,
- "Simtshausen": null,
- "Sinaia": null,
- "Sindelfingen": 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,
- "Sint-Denijs-Boekel": null,
- "Sinzheim": null,
- "Sinzheim Nord": null,
- "Sinzig(Rhein)": null,
- "Sinzing": null,
- "Sion": null,
- "Sipplingen": null,
- "Sissach": null,
- "Sittard": null,
- "Sitzendorf-Unterweißbach": null,
- "Skaerbaek st": null,
- "Skanderborg st": null,
- "Slagelse st": null,
- "Slavonski Brod": null,
- "Sliedrecht": null,
- "Sliedrecht Baanhoek": null,
- "Slubice": null,
- "Sluknov": null,
- "Sluknov zast.": null,
- "Sneek": null,
- "Sneek Noord": null,
- "Soest": null,
- "Soest Zuid": null,
- "Soest(NL)": null,
- "Soestdijk": null,
- "Sohl": null,
- "Sohland": null,
- "Sokolov": null,
- "Solingen Grünewald": null,
- "Solingen Hbf": null,
- "Solingen Mitte": null,
- "Solingen Vogelpark": null,
- "Solingen-Schaberg": null,
- "Sollstedt": null,
- "Solms": null,
- "Solnhofen": null,
- "Solothurn": null,
- "Solpke": null,
- "Soltau Nord": null,
- "Soltau(Han)": null,
- "Soltendieck": null,
- "Somain": 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,
- "Sopot": null,
- "Sopron": null,
- "Sorge": null,
- "Soroe st": null,
- "Sosnowiec Glowny": null,
- "Sottrum": null,
- "Soyen": null,
- "Spa": null,
- "Spaichingen": null,
- "Spaichingen Mitte": null,
- "Spangsbjerg st": null,
- "Sparrieshoop": null,
- "Spaubeek": null,
- "Spay": null,
- "Spechtritz": null,
- "Speele": null,
- "Speicher": null,
- "Speikern": null,
- "Speyer Hbf": null,
- "Speyer Nord-West": null,
- "Spicak": null,
- "Spich": null,
- "Spiegelau": null,
- "Spielberg": null,
- "Spielfeld-Straß": null,
- "Spiez": null,
- "Spinnerei, Ettlingen": null,
- "Spital am Pyhrn": null,
- "Spittal-Millstättersee": null,
- "Sponholz": null,
- "Spornitz": null,
- "Spremberg": null,
- "Sprendlingen(Rheinhess)": null,
- "Springe": null,
- "Sprötze": null,
- "Spöck Hochhaus, Stutensee": null,
- "Spöck Richard-Hecht-Schule, Stutensee": null,
- "St Alban": null,
- "St Avold": null,
- "St Dalmas de Tende": 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 Malo": null,
- "St Mang": null,
- "St Michaelisdonn": null,
- "St Ottilien": null,
- "St Thomas": null,
- "St Wendel": null,
- "St-Amour": null,
- "St-Avre-la-Chambre": null,
- "St-Germain-des-Fosses": null,
- "St-Gervais-les-Bains": null,
- "St-Jean-de-Luz-Ciboure": null,
- "St-Jean-de-Maurienne Arvan": null,
- "St-Jory(Toulouse)": null,
- "St-Louis (Haut-Rhin)": null,
- "St-Louis-la-Chaussee": null,
- "St-Maurice(CH)": null,
- "St-Michel-Valloire": null,
- "St-Pierre-dAlbigny": null,
- "St-Pierre-des-Corps": null,
- "St-Priest": null,
- "St-Quentin(Aisne)": null,
- "St-Raphael-Valescure": null,
- "St-Sulpice-Lauriere": null,
- "St. Anton am Arlberg": null,
- "St. Anton im Montafon": null,
- "St. Gallen(CH)": null,
- "St. Gallen(CH) Haggen": null,
- "St. Gallen(CH) Winkeln": null,
- "St. Johann im Pongau": null,
- "St. Johann in Tirol": null,
- "St. Margrethen SG": null,
- "St. Moritz": null,
- "St. Valentin": null,
- "St.Jodok am Brenner": null,
- "St.Michael in Obersteiermark": null,
- "St.Peter-Seitenstetten": null,
- "St.Pölten Hbf": null,
- "St.Veit/Glan": null,
- "Staad": null,
- "Stade": null,
- "Stadt Rottenmann": null,
- "Stadt Wehlen(Sachs)": null,
- "Stadtallendorf": null,
- "Stadthagen": null,
- "Stadtilm": null,
- "Stadtoldendorf": null,
- "Stadtprozelten": null,
- "Stadtroda": null,
- "Staffel": null,
- "Staffelfelden": null,
- "Stahringen": null,
- "Stainach-Irdning": null,
- "Stambach": null,
- "Stammbach": null,
- "Stams": null,
- "Stans bei Schwaz": null,
- "Stapelburg": null,
- "Stara Role": null,
- "Starckstraße, Karlsruhe": null,
- "Starnberg": null,
- "Starnberg Nord": null,
- "Statte": null,
- "Stauchitz": null,
- "Staudernheim": null,
- "Staufen": null,
- "Staufen Süd": null,
- "Stavoren": null,
- "Staßfurt": null,
- "Steckborn": null,
- "Steckborn URh": null,
- "Stederdorf(Kr Uelzen)": null,
- "Stedum": null,
- "Steenwijk": null,
- "Stegenwaldhaus": null,
- "Steilküste/Wittenbeck": null,
- "Stein(Traun)": null,
- "Stein-Säckingen": null,
- "Steina": null,
- "Steinach in Tirol": 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,
- "Steinbourg": null,
- "Steindorf bei Straßwalchen": null,
- "Steinebach": null,
- "Steinebrunn(CH)": 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,
- "Stelle": null,
- "Stelle DHE": null,
- "Stendal Hbf": null,
- "Stendal Vorbf": null,
- "Stendal-Stadtsee": null,
- "Stenn": null,
- "Stephansfeld": null,
- "Sterbfritz": null,
- "Sternfeld": null,
- "Sternhaus-Haferfeld": null,
- "Sternhaus-Ramberg": null,
- "Sterzhausen": null,
- "Stettbach": null,
- "Stetten (b. Haigerloch)": null,
- "Stetten am Heuchelberg": null,
- "Stetten(Donau)": null,
- "Stetten(Schwab)": null,
- "Stetten-Beinstein": null,
- "Stettfeld(Baden)": null,
- "Stettfeld-Weiher": null,
- "Steyr": null,
- "Stiege": null,
- "Stift Keppel-Allenbach": null,
- "Stockach NE": null,
- "Stockau": null,
- "Stockdorf": null,
- "Stockerau": null,
- "Stockhausen(Lahn)": null,
- "Stockheim(Oberfr)": null,
- "Stockheim(Unterfr)": null,
- "Stockholm Central": null,
- "Stockstadt(Main)": null,
- "Stockstadt(Rhein)": null,
- "Stolberg(Harz)": null,
- "Stolberg(Rheinl)Gbf": 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,
- "Strasbourg": null,
- "Strasburg(Uckerm)": null,
- "Strasshof": null,
- "Straubing": null,
- "Straubing-Ost": null,
- "Strausberg": null,
- "Strausberg (S)": 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ßgräbchen-Bernsdorf": null,
- "Straßkirchen": null,
- "Straßwalchen": null,
- "Stresa": null,
- "Strizivojna-Vrpolje": null,
- "Strohkirchen": null,
- "Strullendorf": null,
- "Struthütten": null,
- "Stubben": null,
- "Stubbenfelde": null,
- "Stubersheim": null,
- "Stumsdorf": null,
- "Sturovo": 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 Nürnberger Str.": null,
- "Stuttgart Schwabstr.": null,
- "Stuttgart Stadtmitte": null,
- "Stuttgart Universität": null,
- "Stuttgart-Bad Cannstatt": null,
- "Stuttgart-Feuerbach": null,
- "Stuttgart-Münster": null,
- "Stuttgart-Obertürkheim": null,
- "Stuttgart-Rohr": null,
- "Stuttgart-Sommerrain": 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,
- "Sulz-Röthis": 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,
- "Sulzbachtal": null,
- "Sulzberg": null,
- "Sulzfeld(Baden)": null,
- "Summerau": null,
- "Sursee": null,
- "Survilliers Fosses": null,
- "Susteren": null,
- "Svatava": null,
- "Svatava zastavka": null,
- "Svor": null,
- "Swalmen": null,
- "Swiebodzin": null,
- "Swinoujscie Centrum": null,
- "Swisttal-Odendorf": null,
- "Syke": null,
- "Syrau": null,
- "Sythen": null,
- "Szczecin Glowny": null,
- "Szczecin Gumience": null,
- "Szentgotthárd": null,
- "Szob": null,
- "Szob(Gr)": null,
- "Szolnok": null,
- "Sättelstädt": null,
- "Sélestat": null,
- "Södertälje Syd station": null,
- "Söllingen Kapellenstraße": null,
- "Söllingen Reetzstr.": 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,
- "TGV Haute Picardie": null,
- "Taben": null,
- "Tabor": null,
- "Tacherting": null,
- "Tamines": 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,
- "Tarascon sur Rhone": null,
- "Tarp": null,
- "Tarvisio Boscoverde": null,
- "Tata": null,
- "Tatabanya": null,
- "Tating": null,
- "Taubenheim(Spree)": null,
- "Tauberbischofsheim": null,
- "Tauberfeld": null,
- "Taucha(Leipzig)": null,
- "Taufkirchen": null,
- "Taufkirchen an der Pram": null,
- "Tautenhain": null,
- "Taverne-Torricella": null,
- "Taxenbach-Rauris": null,
- "Tczew": null,
- "Tecknau": null,
- "Tegelen": null,
- "Tegernsee": null,
- "Teicha": null,
- "Teichland": null,
- "Teichwolframsdorf": null,
- "Teisendorf": null,
- "Teisnach": null,
- "Teisnach Rohde+Schwarz": null,
- "Telfs-Pfaffenhofen": null,
- "Telgte": null,
- "Teltow": null,
- "Teltow Stadt": null,
- "Temmels": null,
- "Templeuve": null,
- "Templin": null,
- "Templin Stadt": null,
- "Templin-Ahrensdorf": null,
- "Tende(F)": null,
- "Teningen-Mundingen": null,
- "Tenneck": null,
- "Teplice v Cechach": null,
- "Terborg": null,
- "Terfens-Weer": null,
- "Tergnier": null,
- "Teschenhagen": null,
- "Teschow": null,
- "Tessin": null,
- "Tessin West": null,
- "Testelt": null,
- "Teterow": null,
- "Teting (Moselle)": 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(Erzgeb)": null,
- "Thalheim(b Oschatz)": null,
- "Thalwil": 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,
- "Thionville": null,
- "Thoßfell": null,
- "Thun": null,
- "Thusis": null,
- "Thyrow": null,
- "Thüngersheim": null,
- "Thür": null,
- "Tiebensee": null,
- "Tiefenau": null,
- "Tiefenbach(b Passau)": null,
- "Tiefenbachmühle": null,
- "Tiefenort": null,
- "Tieffenbach-Struth": null,
- "Tiel": null,
- "Tiel Passewaaij": null,
- "Tienen": null,
- "Tiengen(Hochrhein)": null,
- "Tilburg": null,
- "Tilburg Reeshof": null,
- "Tilburg Universiteit": null,
- "Timmendorfer Strand": null,
- "Tinglev st": null,
- "Tisis": null,
- "Titisee": null,
- "Tittmoning-Wiesmühl": null,
- "Tivoli, Karlsruhe": null,
- "Tjaereborg st": null,
- "Tobel-Affeltrangen": null,
- "Toender Nord st": null,
- "Toender st": null,
- "Tongeren": null,
- "Torgau": null,
- "Torgelow": null,
- "Torino Porta Susa": null,
- "Tornesch": null,
- "Tostedt": null,
- "Toul": null,
- "Toulon": null,
- "Toulouse-Matabiau": null,
- "Tourcoing": null,
- "Tournai": null,
- "Tournan": null,
- "Traben-Trarbach": null,
- "Trabitz": null,
- "Trais-Horloff": null,
- "Trasadingen": null,
- "Trassenheide": null,
- "Trassenmoor": null,
- "Traun": null,
- "Traun OÖ": null,
- "Traundorf": null,
- "Traunreut": null,
- "Traunstein": null,
- "Traunstein Klinikum": null,
- "Trbovlje": null,
- "Trebbin": null,
- "Treben-Lehma": null,
- "Trebgast": null,
- "Trebitz(Elbe)": null,
- "Trebnitz(Mark)": null,
- "Trebusice": null,
- "Trechtingshausen": null,
- "Treibach-Althofen": null,
- "Treis-Karden": null,
- "Trento": null,
- "Treuchtlingen": null,
- "Treuen": null,
- "Treuenbrietzen": null,
- "Treuenbrietzen Süd": null,
- "Treviso Centrale": null,
- "Treysa": null,
- "Triangel": null,
- "Triberg": null,
- "Trieben": 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,
- "Troisvierges": null,
- "Trompet": null,
- "Trooz": null,
- "Trossingen Bahnhof": null,
- "Trossingen Stadt": null,
- "Trostberg": null,
- "Tschagguns": null,
- "Tullastraße/Verkehrsbetriebe, Karlsruhe": null,
- "Tulling": null,
- "Tulln a.d.Donau": null,
- "Tullnerfeld": null,
- "Tuplice": null,
- "Tuplice Debinka": null,
- "Turgi": null,
- "Tuttlingen": null,
- "Tuttlingen Gänsäcker": null,
- "Tuttlingen Nord": null,
- "Tuttlingen Schulen": null,
- "Tuttlingen Zentrum": null,
- "Tutzing": null,
- "Twello": null,
- "Twiste": null,
- "Twistringen": null,
- "Tychy": 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,
- "Uckange": null,
- "Uder": null,
- "Udine": 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,
- "Uhyst": null,
- "Uitgeest": null,
- "Uithuizen": null,
- "Uithuizermeeden": null,
- "Ulberndorf": null,
- "Ulbersdorf": null,
- "Ulm Hbf": null,
- "Ulm Ost": null,
- "Ulm-Donautal": null,
- "Ulm-Söflingen": null,
- "Ulmerfeld-Hausmening": null,
- "Ulrichsbrücke-Füssen": null,
- "Ulzburg Süd": 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,
- "Unterberg-Stefansbrücke": 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,
- "Unterneustädter Kirchplatz, Kassel": null,
- "Unterreichenbach": null,
- "Unterschleißheim": null,
- "Untersteinach(Bayr)": null,
- "Untersteinach(b Stadtsteinach)": null,
- "Unterterzen": null,
- "Unterwellenborn": null,
- "Unterwiesenthal": null,
- "Unteröwisheim Bf": null,
- "Unteröwisheim M.-Luther-Str.": null,
- "Unzmarkt": 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,
- "Usquert": null,
- "Usseln": null,
- "Usti nad Labem hl.n.": null,
- "Usti nad Labem zapad": null,
- "Usti nad Labem-Strekov": null,
- "Utrecht Centraal": null,
- "Utrecht Leidsche Rijn": null,
- "Utrecht Lunetten": null,
- "Utrecht Maliebaan": null,
- "Utrecht Overvecht": null,
- "Utrecht Terwijde": null,
- "Utrecht Vaartsche Rijn": null,
- "Utrecht Zuilen": null,
- "Utting": null,
- "Uttwil": null,
- "Utzedel": null,
- "Vac": null,
- "Vach": null,
- "Vacha": null,
- "Vachdorf": null,
- "Vachendorf": null,
- "Vahldorf": null,
- "Vaihingen(Enz)": null,
- "Vaihingen(Enz)Nord": null,
- "Vaires Torcy": null,
- "Val-de-Reuil": null,
- "Valdaora-Anterselva/Olang-Antholz": null,
- "Valdek": null,
- "Valence TGV": null,
- "Valence Ville": null,
- "Valenciennes": null,
- "Valkenburg(NL)": null,
- "Vallendar": null,
- "Vamdrup st": null,
- "Vandans": null,
- "Varangeville-St-Nicolas": null,
- "Varde Kaserne st": null,
- "Varde Vest st": null,
- "Varde st": null,
- "Varel(Oldb)": null,
- "Varnsdorf": null,
- "Varnsdorf Pivovar Kocour": null,
- "Varnsdorf stare nadr": null,
- "Varsseveld": null,
- "Vastorf": null,
- "Vaterstetten": null,
- "Vatterode": null,
- "Vatteröder Teich": null,
- "Vechelde": null,
- "Vechta": null,
- "Vechta-Stoppelmarkt": null,
- "Veendam": null,
- "Veenendaal Centrum": null,
- "Veenendaal West": null,
- "Veenendaal-De Klomp": null,
- "Vehlefanz": null,
- "Veilsdorf": null,
- "Veitshöchheim": null,
- "Vejle st": null,
- "Velbert Rosenhügel": null,
- "Velbert-Langenberg": null,
- "Velbert-Neviges": null,
- "Velbert-Nierenhof": null,
- "Velden am Wörther See": null,
- "Velden(b Hersbruck)": null,
- "Velgast": null,
- "Velke Zernoseky": null,
- "Velky Senov": null,
- "Velky Senov zast.": null,
- "Vellmar-Niedervellmar": null,
- "Vellmar-Obervellmar": null,
- "Vellmar-Osterberg/EKZ": null,
- "Velp": null,
- "Velten(Mark)": null,
- "Vendenheim": null,
- "Venezia Mestre": null,
- "Venezia Santa Lucia": null,
- "Venissieux": null,
- "Venlo": null,
- "Venray": null,
- "Ventimiglia": null,
- "Ventschow": null,
- "Vercelli": null,
- "Verden(Aller)": null,
- "Veringendorf": null,
- "Veringenstadt": null,
- "Vernante": null,
- "Vernawahlshausen": null,
- "Verneuil lEtang": null,
- "Verneuil sur Avre": null,
- "Vernon(Eure)": null,
- "Verona Porta Nuova": null,
- "Verviers Central": null,
- "Verviers-Palais": null,
- "Vesele pod Rabstejnem": null,
- "Vetschau": null,
- "Vettweiß": null,
- "Vettweiß-Jakobwüllesheim": null,
- "Vicenza": null,
- "Viechtach": null,
- "Vienenburg": null,
- "Vienne": null,
- "Vierenstraße": null,
- "Vierkirchen-Esterhofen": null,
- "Vierlingsbeek": null,
- "Viernau": null,
- "Viersen": null,
- "Vieselbach": null,
- "Vievola": null,
- "Vilemov u Sluknova": null,
- "Villabassa-Braies/Niederdorf-Prags": null,
- "Villach Hbf": null,
- "Villach Warmbad": null,
- "Villach Westbf": null,
- "Villars les Dombes": null,
- "Villedieu les Poeles": null,
- "Villers Cotterets": null,
- "Villiers-le-Bel-Gonesse": null,
- "Villingen(Schwarzw)": null,
- "Villingen-Schwenningen Eisstadion": null,
- "Villingen-Schwenningen Hammerstatt": null,
- "Villmar": null,
- "Vils Stadt": null,
- "Vils in Tirol": null,
- "Vilsbiburg": null,
- "Vilseck": null,
- "Vilshofen(Niederbay)": null,
- "Vilvoorde": null,
- "Vinkovci": null,
- "Vinzelberg": null,
- "Vipiteno-Val di Vizze/Sterzing-Pfitsch": null,
- "Visby st": null,
- "Vise": null,
- "Visp": null,
- "Visselhövede": null,
- "Vitry le François Gare": null,
- "Vittel": null,
- "Vleuten": null,
- "Vlissingen": null,
- "Vlissingen Souburg": null,
- "Vlotho": null,
- "Voerde(Niederrhein)": null,
- "Voerendaal": null,
- "Vogelsang(Gransee)": null,
- "Vogelweh": null,
- "Vohburg": null,
- "Vohren": null,
- "Voigtsgrün": null,
- "Voigtstedt": null,
- "Vojens st": null,
- "Vojtanov": null,
- "Voldagsen": null,
- "Volders-Baumkirchen": null,
- "Volkach-Astheim": null,
- "Volkmarsen": null,
- "Volkringhausen": null,
- "Volkswohnung/Staatstheater, Karlsruhe": null,
- "Volpriehausen": null,
- "Voorburg": null,
- "Voorhout": null,
- "Voorschoten": null,
- "Voorst-Empe": null,
- "Vorden": null,
- "Vorhop": null,
- "Vormwald": null,
- "Vormwald Dorf": null,
- "Vorra(Pegnitz)": null,
- "Voßloch": null,
- "Vriezenveen": null,
- "Vroegum st": null,
- "Vroomshoop": null,
- "Vught": null,
- "Vysoka Pec": null,
- "Vöcklabruck": null,
- "Vöcklamarkt": 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,
- "Völs": null,
- "Wabern(Bz Kassel)": null,
- "Wachenheim(Pfalz)": null,
- "Wackershofen": null,
- "Waddinxveen": null,
- "Waddinxveen Noord": null,
- "Waddinxveen Triangel": null,
- "Waffenbrunn": null,
- "Wagersrott": 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,
- "Wald am Schoberpass": null,
- "Walddrehna": null,
- "Waldenburg(Sachs)": 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-Kraiburg": null,
- "Waldmünchen": null,
- "Waldshut": null,
- "Walenstadt": null,
- "Walferdange": null,
- "Walhausen(Saar)": null,
- "Walheim(Württ)": null,
- "Walkenried": null,
- "Wallau(Lahn)": null,
- "Walldorf(Hess)": null,
- "Walldorf(Werra)": null,
- "Walldürn": null,
- "Wallenrod": null,
- "Wallersdorf": null,
- "Wallertheim": null,
- "Walleshausen": null,
- "Wallhausen(Helme)": null,
- "Wallhausen(Württ)": null,
- "Wallisellen": null,
- "Wallwitz(Saalkr)": null,
- "Walpertskirchen": null,
- "Walporzheim": null,
- "Walschleben": null,
- "Walsleben": null,
- "Walsrode": null,
- "Waltershausen": null,
- "Waltershausen Schnepfenthal": null,
- "Walygator Parc": null,
- "Wandersleben": null,
- "Wandlitz": null,
- "Wandlitzsee": null,
- "Wangen(Allgäu)": null,
- "Wangen(Unstrut)": null,
- "Wangerooge": 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,
- "Warffum": null,
- "Warmbad": null,
- "Warnemünde": null,
- "Warnemünde Werft": null,
- "Warngau": null,
- "Warnitz(Uckermark)": null,
- "Warszawa Centralna": null,
- "Warszawa Wschodnia": null,
- "Warszawa Zachodnia": null,
- "Wartberg im Mürztal": null,
- "Wartberg/Krems": null,
- "Warthausen": null,
- "Wasbek": null,
- "Wasen, Ettlingen": null,
- "Wasenweiler": null,
- "Wasseralfingen": null,
- "Wasserbillig": 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,
- "Watenstedt": null,
- "Waterloo": null,
- "Watermael/Watermaal": null,
- "Wattenscheid": null,
- "Wattenscheid-Höntrop": null,
- "Watzelsteg": null,
- "Watzenborn-Steinberg": null,
- "Waßmannsdorf": null,
- "Webau": null,
- "Wecker": null,
- "Weckesheim": null,
- "Weddel(Braunschw)": null,
- "Wedel(Holst)": null,
- "Weener": null,
- "Weert": null,
- "Weesenstein": null,
- "Weesp": null,
- "Weetzen": null,
- "Weeze": null,
- "Wefensleben": null,
- "Wega": null,
- "Wegberg": null,
- "Wegeleben": null,
- "Wegenstedt": null,
- "Wegliniec": null,
- "Wehdel": null,
- "Wehl": null,
- "Wehr(Mosel)": null,
- "Wehr-Brennet": 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,
- "Weinbrennerplatz, Karlsruhe": null,
- "Weinböhla Hp": null,
- "Weinfelden": null,
- "Weingarten Berg": null,
- "Weingarten(Baden)": null,
- "Weinheim(Bergstr)Hbf": null,
- "Weinheim-Lützelsachsen": null,
- "Weinsberg": null,
- "Weinsberg West": null,
- "Weinsberg/Ellhofen Gewerbegebiet": null,
- "Weinweg, Karlsruhe": 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,
- "Welkenraedt": null,
- "Welkers": null,
- "Wellen(Magdeburg)": null,
- "Wellen(Mosel)": null,
- "Wellendorf": null,
- "Wellmitz": null,
- "Wels Hbf": null,
- "Welschen Ennest": null,
- "Welschingen-Neuhausen": null,
- "Welver": null,
- "Wemmetsweiler Rathaus": null,
- "Wendisch Evern": null,
- "Wendisch-Rietz": null,
- "Wendling b.Haag": null,
- "Wendlingen(Neckar)": null,
- "Wennedach": null,
- "Wennigsen(Deister)": null,
- "Wensickendorf": null,
- "Werbig": null,
- "Werdau": null,
- "Werdau Nord": null,
- "Werder(Havel)": null,
- "Werderstraße, Karlsruhe": null,
- "Werdohl": null,
- "Werdorf": null,
- "Werfen": null,
- "Werl": null,
- "Wernau(Neckar)": null,
- "Wernberg": null,
- "Werne a d Lippe": null,
- "Werneuchen": null,
- "Wernfeld": null,
- "Wernigerode Elmowerk": null,
- "Wernigerode Hbf": null,
- "Wernigerode Hochschule Harz": null,
- "Wernigerode Westerntor": null,
- "Wernigerode-Hasserode": null,
- "Wernshausen": null,
- "Wernstein": null,
- "Wertach-Haslach": null,
- "Wertheim": null,
- "Wertheim-Bestenheid": null,
- "Werther": null,
- "Wesel": null,
- "Wesel Feldmark": null,
- "Wesel-Blumenkamp": null,
- "Wesenberg": null,
- "Wespelaar-Tildonk": null,
- "Wesselburen": null,
- "Wesseln": null,
- "Westbarthausen": null,
- "Westbevern": null,
- "Westendorf": null,
- "Westendorf in Tirol": null,
- "Westerburg": null,
- "Westerham": null,
- "Westerhausen": null,
- "Westerland (Sylt) Autoverladung": null,
- "Westerland(Sylt)": null,
- "Westerstede-Ocholt": null,
- "Westerstetten": null,
- "Westervoort": 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,
- "Wettingen": null,
- "Wetzlar": null,
- "Wezep": null,
- "Weßling(Oberbay)": null,
- "Wickede(Ruhr)": null,
- "Wicklesgreuth": null,
- "Wickrath": null,
- "Wiebelskirchen": null,
- "Wiemersdorf": null,
- "Wien Floridsdorf": null,
- "Wien Franz-Josefs-Bahnhof": null,
- "Wien Hbf": null,
- "Wien Hbf (Autoreisezuganlage)": null,
- "Wien Hernals": null,
- "Wien Hütteldorf": null,
- "Wien Jedlersdorf": null,
- "Wien Kaiserebersdorf": null,
- "Wien Meidling": null,
- "Wien Mitte": null,
- "Wien Penzing": null,
- "Wien Praterstern": null,
- "Wien Simmering": null,
- "Wien Stadlau": null,
- "Wien Süßenbrunn": null,
- "Wien Westbahnhof": null,
- "Wiener Neustadt Hbf": null,
- "Wiener Straße, Kassel": null,
- "Wierden": 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,
- "Wijchen": null,
- "Wijhe": null,
- "Wil SG": 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,
- "Wildon": null,
- "Wilferdingen-Singen": null,
- "Wilgartswiesen": null,
- "Wilhelmsdorf": null,
- "Wilhelmshaven": null,
- "Wilhelmshorst": null,
- "Wilhelmshütte(Lahn)": null,
- "Wilhelmsstraße/Stadtmuseum, Kassel": 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,
- "Wilnsdorf-Rudersdorf": null,
- "Wilsenroth": null,
- "Wilster": null,
- "Wilthen": null,
- "Wiltingen(Saar)": null,
- "Wilwerwiltz": null,
- "Wilwisheim": null,
- "Wincheringen": null,
- "Winden(Pfalz)": null,
- "Windischeschenbach": null,
- "Windischgarsten": null,
- "Windsbach": null,
- "Wingen-sur-Moder": null,
- "Wingerode": null,
- "Wingst": null,
- "Winkelhaid": null,
- "Winnenden": null,
- "Winningen(Mosel)": null,
- "Winninghausen": null,
- "Winnweiler": null,
- "Winschoten": null,
- "Winsen(Luhe)": null,
- "Winsum": null,
- "Winterbach(b Schorndorf)": null,
- "Winterberg(Westf)": null,
- "Winterhausen": null,
- "Wintermoor": null,
- "Winterswijk": null,
- "Winterswijk West": null,
- "Winterthur": null,
- "Wipperdorf": null,
- "Wippra": null,
- "Wirges": null,
- "Wirtheim": null,
- "Wismar": null,
- "Wissembourg": null,
- "Wissen(Sieg)": null,
- "Wissingen": null,
- "Wittbräucke": null,
- "Witten Hbf": null,
- "Witten-Annen Nord": null,
- "Wittenbach": 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,
- "Wjasma": null,
- "Woerden": null,
- "Woffleben": null,
- "Wohlen AG": null,
- "Wohltorf": null,
- "Woippy": null,
- "Wolfach": null,
- "Wolfartsweierer Straße, Karlsruhe": null,
- "Wolfegg": null,
- "Wolfen(Bitterfeld)": null,
- "Wolfenbüttel": null,
- "Wolferode": null,
- "Wolfgang(Kr Hanau)": null,
- "Wolfhagen": null,
- "Wolfheze": null,
- "Wolfratshausen": null,
- "Wolfsburg Hbf": null,
- "Wolfsgefärth": null,
- "Wolfsmünster": null,
- "Wolfstee": null,
- "Wolfstein": null,
- "Wolfurt": null,
- "Wolgast": null,
- "Wolgast Hafen": null,
- "Wolgaster Fähre": null,
- "Wolkenstein": null,
- "Wolkersdorf im Weinviertel": null,
- "Wolkramshausen": null,
- "Wollbach(Baden)": null,
- "Wolmirstedt": null,
- "Wolterdingen(Han)": null,
- "Woltersdorf/Nuthe-Urstromtal": null,
- "Woltwiesche": null,
- "Wolvega": null,
- "Workum": null,
- "Wormerveer": null,
- "Worms Hbf": null,
- "Worms-Pfeddersheim": null,
- "Worpswede": null,
- "Wremen": null,
- "Wriezen": null,
- "Wrist": null,
- "Wroclaw Glowny": null,
- "Wroclaw Lesnica": null,
- "Wroclaw Nowy Dwor": null,
- "Wulfen(Anh)": null,
- "Wulfen(Westf)": null,
- "Wulften": null,
- "Wullenstetten": null,
- "Wunsiedel-Holenbrunn": null,
- "Wunstorf": null,
- "Wuppertal Hbf": null,
- "Wuppertal-Barmen": null,
- "Wuppertal-Hahnenfurth/Düssel": 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)": 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ädenswil": null,
- "Wölfershausen": null,
- "Wölfersheim-Södel": null,
- "Wörgl Hbf": 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,
- "Ybbs a.d. Donau": null,
- "Yorckstraße, Karlsruhe": null,
- "Yverdon-les-Bains": null,
- "Yves-Gomezee": null,
- "ZOB, Duderstadt": null,
- "Zaandam": null,
- "Zaandam Kogerveld": null,
- "Zaandijk Zaanse Schans": null,
- "Zabeltitz": null,
- "Zachun": null,
- "Zagan": null,
- "Zagorje": null,
- "Zagreb Glavni kolodvor": null,
- "Zahna": null,
- "Zainhammer": null,
- "Zaisenhausen": null,
- "Zaltbommel": null,
- "Zandvoort aan Zee": null,
- "Zapfendorf": null,
- "Zarrendorf": null,
- "Zary": null,
- "Zasieki": null,
- "Zawiercie": null,
- "Zbaszynek": null,
- "Zedelgem": null,
- "Zeebrugge-Dorp": null,
- "Zeesen": null,
- "Zehdenick(Mark)": null,
- "Zehdenick-Neuhof": null,
- "Zeil": null,
- "Zeithain": null,
- "Zeitz": null,
- "Zelezna Ruda centrum": null,
- "Zelezna Ruda mesto": null,
- "Zell am See": null,
- "Zell am Ziller": null,
- "Zell(Harmersbach)": null,
- "Zell(Wiesental)": null,
- "Zell-Romrod": null,
- "Zella-Mehlis": null,
- "Zella-Mehlis West": null,
- "Zellendorf": null,
- "Zellerthal": null,
- "Zeltweg": null,
- "Zempin": null,
- "Zennern": null,
- "Zepernick(Bernau)": null,
- "Zeppelinheim": null,
- "Zerbst/Anhalt": null,
- "Zerkall": null,
- "Zermatt": null,
- "Zernsdorf": null,
- "Zerrenthin": null,
- "Zetten-Andelst": null,
- "Zeulenroda unt Bf": null,
- "Zeutern Bf": null,
- "Zeutern Ost": null,
- "Zeutern Sportplatz": null,
- "Zeuthen": null,
- "Zeutsch": null,
- "Zevenaar": null,
- "Zevenbergen": null,
- "Zgorzelec": null,
- "Zgorzelec Miasto": null,
- "Zichem": null,
- "Zidani Most": null,
- "Ziegelbrücke": null,
- "Zielitz": null,
- "Zielitz Ort": null,
- "Zielona Gora Gl.": null,
- "Zierenberg": null,
- "Zierenberg-Rosental": null,
- "Ziesar": null,
- "Zieverich": null,
- "Zillendorf": null,
- "Ziltendorf": null,
- "Zimmern(Main-Tauber)": null,
- "Zimmern(b Seckach)": null,
- "Zimmersrode": null,
- "Zinnowitz": null,
- "Zirl": null,
- "Zirndorf": null,
- "Zirndorf Kneippallee": null,
- "Zirovice-Seniky": null,
- "Zirtow-Leussow": null,
- "Zittau": null,
- "Zittau Hp": null,
- "Zittau Süd": null,
- "Zittau Vorstadt": null,
- "Zizers": null,
- "Zoblitz": null,
- "Zoetermeer": null,
- "Zoetermeer Oost": null,
- "Zofingen": null,
- "Zolder": null,
- "Zollhaus(Villingen-Schwenningen)": null,
- "Zollhaus-Petersthal": null,
- "Zopten": null,
- "Zorneding": null,
- "Zossen": null,
- "Zotzenbach": null,
- "Zschaitz": null,
- "Zscherben": null,
- "Zschopau": null,
- "Zschopau Ost": null,
- "Zschortau": null,
- "Zug(CH)": null,
- "Zuidbroek": null,
- "Zuidhorn": null,
- "Zusenhofen": null,
- "Zutphen": null,
- "Zuzenhausen": null,
- "Zweibrücken Hbf": null,
- "Zweidlen": 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,
- "Zwijndrecht(NL)": null,
- "Zwingenberg(Baden)": null,
- "Zwingenberg(Bergstr)": null,
- "Zwolle": null,
- "Zwolle Stadshagen": null,
- "Zwota": null,
- "Zwota-Zechenbach": null,
- "Zwotental": null,
- "Zwönitz": null,
- "Zöberitz": null,
- "Zörnigall": null,
- "Zühlsdorf": null,
- "Zülpich": null,
- "Zürich Altstetten": null,
- "Zürich Enge": null,
- "Zürich Flughafen": null,
- "Zürich HB": null,
- "Zürich Hardbrücke": null,
- "Zürich Oerlikon": null,
- "Zürich Stadelhofen": null,
- "Zürich Wiedikon": null,
- "Zürich Wollishofen": null,
- "Züssow": null,
- "Züttlingen": null,
- "s-Hertogenbosch": null,
- "s-Hertogenbosch Oost": null,
- "t Harde": null,
- "Äpfingen": null,
- "Öhringen Hbf": null,
- "Öhringen West": null,
- "Öhringen-Cappel": null,
- "Ölbronn-Dürrn": null,
- "Ötigheim": null,
- "Ötisheim": null,
- "Ötztal": null,
- "Übach-Palenberg": null,
- "Überlingen": null,
- "Überlingen Therme": null,
- "Überlingen-Nußdorf": null,
- "Übersee": null,
- "Ückeritz": null,
- "Üdingen": null,
- "Ürzig(DB)": null,
-
- }
- });
-});
diff --git a/public/static/js/autocomplete.min.js b/public/static/js/autocomplete.min.js
deleted file mode 100644
index 7574e4b..0000000
--- a/public/static/js/autocomplete.min.js
+++ /dev/null
@@ -1 +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-Rothe Erde":null,"Aalen Hbf":null,Aalten:null,Aalter:null,Aarau:null,"Aarburg-Oftringen":null,Aarhus:null,Abcoude:null,Abenden:null,Abensberg:null,Achern:null,"Achern Stadt":null,Achiet: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,"Affoltern am Albis":null,Agatharied:null,Agathenburg:null,Agde: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 (S)":null,"Ahrensfelde Friedhof":null,"Ahrensfelde Nord":null,Ahrweiler:null,"Ahrweiler Markt":null,"Aich(Niederbay)":null,Aichach:null,Aichstetten:null,Aigle:null,"Aime-la-Plagne":null,Aindorf:null,Ainring:null,Airolo:null,"Aix-en-Provence TGV":null,"Aix-les-Bains-le-Revard":null,Akkrum:null,"Alba Iulia":null,"Albate-Camerlata":null,Albbruck:null,Albersdorf:null,"Albersweiler(Pfalz)":null,Albertville:null,"Albgaubad, Ettlingen":null,Albig: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,"Alençon":null,Ales:null,Aletshausen:null,Alexisbad:null,"Alfeld(Leine)":null,"Alfter-Impekoven":null,"Alfter-Witterschlick":null,Algermissen:null,Aligse:null,"Alken(B)":null,Alkmaar:null,"Alkmaar Noord":null,"Allendorf(Dillkr)":null,"Allendorf(Eder) Bf":null,Allensbach:null,"Allerheiligenhöfe":null,"Allersberg(Rothsee)":null,Allmendingen:null,Almelo:null,"Almelo de Riet":null,"Almere Buiten":null,"Almere Centrum":null,"Almere Muziekwijk":null,"Almere Oostvaarders":null,"Almere Parkwijk":null,"Almere Poort":null,Alpen:null,"Alphen aan den Rijn":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,"Alt Schwerin":null,Altach:null,Altbach:null,"Altdorf West":null,"Altdorf(CH)":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,"Altendorf(CH)":null,Altenerding:null,"Altenfeld(Rhön)":null,Altenglan:null,"Altengörs":null,Altenhasungen:null,"Altenkirchen(Westerwald)":null,"Altenmarkt im Pongau":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,"Altmarkt/Regierungspräsidium, Kassel":null,Altmittweida:null,Altmorschen:null,Altnau:null,"Altomünster":null,"Altoschatz-Rosenthal":null,Altranft:null,Altshausen:null,"Altstädten(Allgäu)":null,"Altstätten SG":null,Alttann:null,"Altötting":null,Alveslohe:null,"Alvesta station":null,"Alzenau Burg":null,"Alzenau Nord":null,"Alzenau(Unterfr)":null,Alzey:null,"Alzey Süd":null,"Alzey West":null,"Am Kupferhammer, Kassel":null,"Am Stern, Kassel":null,"Am Weinberg, Kassel":null,Amberg:null,Amberieu:null,Amerang:null,"Amersfoort Centraal":null,"Amersfoort Schothorst":null,"Amersfoort Vathorst":null,Ammern:null,Amorbach:null,Ampfing:null,Amriswil:null,Amsdorf:null,"Amsterdam Amstel":null,"Amsterdam Bijlmer ArenA":null,"Amsterdam Centraal":null,"Amsterdam Holendrecht":null,"Amsterdam Lelylaan":null,"Amsterdam Muiderpoort":null,"Amsterdam RAI":null,"Amsterdam Science Park":null,"Amsterdam Sloterdijk":null,"Amsterdam Zuid":null,"Amstetten NÖ":null,"Amstetten(W) Lokalbahn":null,"Amstetten(Württ)":null,Amtshainersdorf:null,Andelfingen:null,Andermatt:null,Andernach:null,Andorf:null,Angermund:null,"Angermünde":null,"Angern-Rogätz":null,Angersbach:null,Angersdorf:null,Angleur:null,Angouleme:null,Anklam:null,"Anna Paulowna":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,"Ans(B)":null,Ansbach:null,Antibes:null,Antonsthal:null,"Antwerpen Centraal":null,"Antwerpen-Berchem":null,"Antwerpen-Zuid":null,Anwanden:null,Anzefahr:null,Anzenkirchen:null,"Apach(Moselle)":null,Apeldoorn:null,"Apeldoorn De Maten":null,"Apeldoorn Osseveld":null,Apensen:null,Apolda:null,Appenweier:null,Appingedam:null,Arad:null,Arbon:null,"Arbon (See)":null,Arbste:null,Ardey:null,"Arensdorf(Köthen)":null,Arenshausen:null,"Arfurt(Lahn)":null,"Argeles-sur-Mer":null,Arkel:null,Arles:null,Arlon:null,Armsheim:null,Arnbach:null,Arnemuiden:null,"Arnhem Centraal":null,"Arnhem Presikhaaf":null,"Arnhem Velperpoort":null,"Arnhem Zuid":null,Arnoldstein:null,"Arnsberg(Westf)":null,Arnschwang:null,"Arnsdorf(Dresden)":null,"Arnstadt Hbf":null,"Arnstadt Süd":null,Arosa:null,Arrach:null,"Arras(F)":null,Arsbeck:null,"Artenay(Loiret)":null,Artern:null,"Arth-Goldau":null,Arvant:null,"Arzberg(Oberfr)":null,"As(CZ)":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,Asse:null,Asselheim:null,Assen:null,"Assenheim(Oberhess)":null,Assmannshausen:null,Attendorn:null,"Attendorn-Hohen Hagen":null,"Attnang-Puchheim":null,"Au SG":null,"Au ZH":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,Auersmacher:null,"Auestadion, Kassel":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-Oberhausen":null,"August-Bebel-Straße, Karlsruhe":null,Augustfehn:null,"Augustusburg Bergstation":null,Aukrug:null,Aulendorf:null,"Aulnoye Aymeries":null,Aumenau:null,"Aumühle":null,"Auneau(Dourdan)":null,"Auringen-Medenbach":null,Auvelais:null,"Auw an der Kyll":null,"Außenried":null,"Avesnes-sur-Helpe":null,"Avignon Centre":null,"Avignon TGV":null,Aying:null,"Aßlar":null,"Aßling(Oberbay)":null,Baabe:null,Baalberge:null,"Baar(CH)":null,"Baar-Ebenhausen":null,Baarn:null,"Babenhausen Langstadt":null,"Babenhausen(Hess)":null,Babstadt:null,Babylon: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 Aussee":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 Blumau":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ürkheim":null,"Bad Dürkheim-Trift":null,"Bad Dürrenberg":null,"Bad Elster":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 Gastein":null,"Bad Griesbach(Schwarzwald)":null,"Bad Grönenbach":null,"Bad Harzburg":null,"Bad Herrenalb":null,"Bad Hersfeld":null,"Bad Hofgastein":null,"Bad Holzhausen":null,"Bad Homburg":null,"Bad Honnef Stadtbahn":null,"Bad Honnef(Rhein)":null,"Bad Höhenstadt":null,"Bad Hönningen":null,"Bad Imnau":null,"Bad Ischl":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 Nieuweschans":null,"Bad Oeynhausen":null,"Bad Oeynhausen Süd":null,"Bad Oldesloe":null,"Bad Peterstal":null,"Bad Pyrmont":null,"Bad Ragaz":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 Salzuflen-Sylbach":null,"Bad Salzungen":null,"Bad Sassendorf":null,"Bad Saulgau":null,"Bad Schallerbach-Wallern":null,"Bad Schandau":null,"Bad Schlema":null,"Bad Schmiedeberg":null,"Bad Schmiedeberg Kurzentrum":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 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 Vigaun":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 Zurzach":null,"Bad Zwischenahn":null,Baddeckenstedt:null,"Baden(CH)":null,"Baden(Verden)":null,"Baden-Baden":null,"Baden-Baden Haueneberstein":null,"Baden-Baden Rebland":null,Baflo:null,Bagenz:null,"Bahlingen Riedlen":null,"Bahlingen am Kaiserstuhl":null,"Bahnbrücken":null,"Bahnhof Niederzwehren, Kassel":null,"Bahnhof, Gönnheim":null,Bahnsdorf:null,Baierbrunn:null,"Baiersbronn Bf":null,"Baiersbronn Schule":null,Baiersdorf:null,Baisieux:null,Baitz:null,Balbersdorf:null,Baldham:null,Balduinstein:null,Balerna:null,Balgheim:null,"Balgstädt":null,"Balingen Süd":null,"Balingen(Württ)":null,Ballersbach:null,"Ballstädt(Gotha)":null,Baltersweiler:null,Balve:null,Bamberg:null,Bammental:null,"Bannemin-Mölschow":null,"Banova Jaruga":null,"Bansin Seebad":null,Banteln:null,Bantin:null,Bantorf:null,Bantzenheim:null,"Banyuls-sur-Mer":null,"Bar-le-Duc":null,Barabein:null,Barbelroth:null,Barby:null,"Barcelona Sants":null,Barchel:null,Bardowick:null,Barendrecht:null,Bargstedt:null,Bargteheide:null,Barleben:null,"Barleber See":null,Barmstedt:null,"Barmstedt Brunnenstr":null,"Barneveld Centrum":null,"Barneveld Noord":null,"Barneveld Zuid":null,"Barnstorf(Han)":null,Barnten:null,Barrien:null,Barsinghausen:null,"Bartenheim(Bale)":null,Barth:null,"Barthmühle":null,"Baruth(Mark)":null,"Bascharage-Sanem":null,"Basdahl Kluste":null,Basdorf:null,"Basel Bad Bf":null,"Basel Dreispitz":null,"Basel SBB":null,"Basel St Johann":null,Bassersdorf:null,Bassum:null,"Batzenhäusle":null,Batzhausen:null,Bauerbach:null,Baumholder:null,Baunach:null,"Baunatal-Guntershausen":null,"Baunatal-Rengershausen":null,Baunhoej:null,Bautzen:null,Bavendorf:null,Bayerbach:null,"Bayerisch Eisenstein":null,"Bayerisch Gmain":null,Bayonne:null,"Bayreuth Hbf":null,"Bayreuth-St Georgen":null,Bayrischzell:null,Bebitz:null,Bebra:null,"Bechstedt-Trippstein":null,"Beckingen(Saar)":null,"Beckum-Neubeckum":null,"Bedburg(Erft)":null,"Bedburg-Hau":null,Bedum:null,"Beek-Elsloo":null,Beelen:null,"Beelitz Stadt":null,"Beelitz-Heilstätten":null,"Beerfelden Hetzbach":null,Beernem:null,Beesd:null,Beeskow:null,"Beetz-Sommerfeld":null,Behringersdorf:null,Beienheim:null,Beilen:null,Beilrode:null,Beimerstetten:null,Bekescsaba:null,"Bela pod Bezdezem":null,Beldorf:null,"Belfort Ville":null,Belgershain:null,Belleben:null,"Bellegarde(Ain)":null,Bellenberg:null,"Belleville Meurthe et Moselle":null,"Bellheim Am Mühlbuckel":null,"Bellheim Bf":null,Bellinzona:null,Belp:null,"Belval Lycée":null,"Belval-Rédange":null,"Belval-Université":null,"Belvaux-Soleuvre":null,Bempflingen:null,Benediktbeuern:null,"Benesov n. Ploucnici":null,Benestroff:null,"Benfeld(Selestat)":null,Bengel:null,Bening: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,"Berchem(LUX)":null,"Berchtesgaden Hbf":null,"Berg(CH)":null,"Berg(Pfalz)":null,"Berga(Elster)":null,"Berga-Kelbra":null,"Bergen auf Rügen":null,"Bergen op Zoom":null,"Bergen(Oberbay)":null,Bergenweiler:null,"Bergfelde(b Berlin)":null,"Berghausen Am Stadion":null,"Berghausen Hummelberg":null,"Berghausen Pfinzbrücke":null,"Berghausen(Baden)":null,"Berghausen(Pfalz)":null,"Berghausen(b Wittgenstein)":null,"Bergheim(Erft)":null,"Bergheim-Giflitz":null,"Bergisch Gladbach":null,Bergsdorf:null,Bergtheim:null,"Bergues(Coudek)":null,Bergwitz:null,"Bergün/Bravuogn":null,"Beringen Bad Bf":null,Beringerfeld:null,Beringhausen:null,Beringstedt:null,"Berka(Wipper)":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 Beusselstraße":null,"Berlin Bornholmer Str.":null,"Berlin Botanischer Garten":null,"Berlin Brandenburger Tor":null,"Berlin Buckower Chaussee":null,"Berlin Bundesplatz":null,"Berlin Charlottenburg (S)":null,"Berlin Eichborndamm":null,"Berlin Feuerbachstr.":null,"Berlin Frankfurter Allee":null,"Berlin Friedrichstraße":null,"Berlin Friedrichstraße (S)":null,"Berlin Gehrenseestraße":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 Ostbahnhof (S)":null,"Berlin Ostkreuz":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 Treptower Park":null,"Berlin Wannsee":null,"Berlin Wannsee (S)":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-Hohenschönhausen (S)":null,"Berlin-Johannisthal":null,"Berlin-Karlshorst":null,"Berlin-Karlshorst (S)":null,"Berlin-Karow":null,"Berlin-Kaulsdorf":null,"Berlin-Köpenick":null,"Berlin-Lankwitz":null,"Berlin-Lichtenberg":null,"Berlin-Lichtenberg (S)":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-Mahlsdorf (S)":null,"Berlin-Marienfelde":null,"Berlin-Marzahn":null,"Berlin-Neukölln":null,"Berlin-Nikolassee":null,"Berlin-Oberspree":null,"Berlin-Pankow":null,"Berlin-Pankow-Heinersdorf":null,"Berlin-Pichelsberg":null,"Berlin-Rahnsdorf":null,"Berlin-Rummelsburg":null,"Berlin-Schlachtensee":null,"Berlin-Schulzendorf":null,"Berlin-Schöneberg":null,"Berlin-Schöneweide":null,"Berlin-Schöneweide (S)":null,"Berlin-Schönholz":null,"Berlin-Spandau":null,"Berlin-Spandau (S)":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,"Berlingen URh":null,"Berlingen(CH)":null,"Bermatingen-Ahausen":null,Bern:null,"Bernau (S)":null,"Bernau a Chiemsee":null,"Bernau(b Berlin)":null,"Bernau-Friedenstal":null,Bernay:null,"Bernburg Hbf":null,"Bernburg-Friedenshall":null,"Bernburg-Roschwitz":null,"Bernburg-Strenzfeld":null,"Bernburg-Waldau":null,Berne:null,Bernried:null,Bernterode:null,Beroun:null,"Bersenbrück":null,Berthelming:null,"Berthelsdorf(Erzgeb)":null,"Berthelsdorf(Erzgebirge) Ort":null,"Bertrange-Strassen":null,Bertrix:null,Bertsdorf:null,Berzhahn:null,"Besançon-Mouillère":null,"Besançon-Viotte":null,Besch:null,Besigheim:null,Besseringen:null,Best:null,Bestensee:null,Bestwig:null,Bettembourg:null,"Bettembourg(fr)":null,"Bettmannsäge":null,Bettwiesen:null,"Betzdorf(LUX)":null,"Betzdorf(Sieg)":null,Beucha:null,Beuchow:null,Beuggen:null,"Beuna(Geiseltal)":null,Beuren:null,Beuron:null,Beutelsbach:null,Beutersitz:null,"Beverungen-Wehrden":null,Beverwijk:null,Bex:null,Bexbach:null,Beyendorf:null,Beziers:null,Biarritz:null,Biasca:null,"Bibelöd":null,"Biberach(Baden)":null,"Biberach(Riß)":null,"Biberach(Riß) Süd":null,"Biberist Ost":null,"Biberist RBS":null,Biblis:null,Bibra:null,Bichl:null,"Bichlbach Almkopfbahn":null,"Bichlbach-Berwang":null,"Bickenbach(Bergstr)":null,Biebesheim:null,Biedenkopf:null,"Biedenkopf Campus":null,Biederitz:null,"Biel/Bienne":null,"Bielefeld Hbf":null,"Bielefeld Ost":null,"Bielefeld-Brackwede":null,"Bielefeld-Senne":null,"Bielefeld-Sennestadt":null,"Bielefeld-Windelsbleiche":null,Biendorf:null,"Bienenbüttel":null,"Bienenmühle":null,Bierbach:null,"Bieren-Rödinghausen":null,Bieringen:null,"Biersdorf(Westerw)":null,"Biersdorf-Ort(Ww)":null,"Bierset-Awans":null,Biesenrode:null,Biesenthal:null,Biessenhofen:null,"Bietigheim(Baden)":null,"Bietigheim-Bissingen":null,Bietingen:null,Bigge:null,Bildstock:null,Bilfingen:null,Bilina:null,Billenhausen:null,Billerbeck:null,"Billum st":null,Bilten:null,Bilthoven:null,"Bily Kostel nad Nisou":null,Binau:null,Bindfelde:null,Bindlach:null,"Bingen(Rhein) Hbf":null,"Bingen(Rhein) Stadt":null,"Bingen-Gaulsheim":null,Binolen: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,"Birmensdorf ZH":null,Birresborn:null,"Bischheim-Gersdorf":null,Bischofshofen: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,"Blainville-Damelevieres":null,Blaj:null,Blankenbach:null,"Blankenberg(Meckl)":null,"Blankenberg(Sieg)":null,"Blankenburg(Harz)":null,"Blankenfelde (S)":null,"Blankenfelde(Teltow-Fläming)":null,"Blankenheim(Sangerhausen)":null,"Blankenheim(Wald)":null,Blankenloch:null,"Blankenloch Kirche, Stutensee":null,"Blankenloch Mühlenweg, Stutensee":null,"Blankenloch Nord, Stutensee":null,"Blankenloch Süd, Stutensee":null,"Blankenloch Tolna-Platz, Stutensee":null,"Blankensee(Meckl)":null,"Blankenstein(Saale)":null,Blaubeuren:null,Blaufelden:null,"Blausee-Mitholz":null,Blaustein:null,"Blechhammer(Thür)":null,Bleibach:null,"Bleichenbach(Oberh)":null,"Bleicherode Ost":null,Blens:null,Blerick:null,"Blieskastel-Lautzkirchen":null,Blindenmarkt:null,Blindheim:null,Bloemendaal:null,Bludenz:null,"Bludenz Brunnenfeld":null,"Bludenz-Moos":null,"Blumberg(b Berlin)":null,"Blumberg-Rehhahn":null,"Blumberg-Riedöschingen":null,"Blumberg-Zollhaus":null,Blumenau:null,Blumenhagen:null,"Blumenthal(Mark)":null,"Blönsdorf":null,Bobenheim:null,Bobingen:null,Bobitz:null,Bobstadt:null,Bobzin:null,Bocholt:null,"Bochum Hbf":null,"Bochum West":null,"Bochum-Dahlhausen":null,"Bochum-Ehrenfeld":null,"Bochum-Hamme":null,"Bochum-Langendreer":null,"Bochum-Langendreer West":null,"Bochum-Riemke":null,"Bockenheim-Kindenheim":null,Bodegraven:null,Bodelsberg:null,Bodelshausen:null,Bodenburg:null,Bodenfelde:null,Bodenheim:null,Bodenmais:null,Bodenrode:null,"Bodenwöhr Nord":null,"Bodio TI":null,"Boen(F)":null,Bogen:null,Bohmte:null,Bohumin:null,Boisheim:null,"Boizenburg(Elbe)":null,Bokholt:null,"Bollwiller(Lutterb)":null,"Bologna Centrale":null,"Bolzano/Bozen":null,"Bondorf(b Herrenberg)":null,"Bonn Brühler Str.":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 UN Campus":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,"Boondael/Boondaal":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,"Bordeaux-St-Jean":null,Bordesholm:null,Borgeln:null,Borgholzhausen:null,"Borgo S. Dalmazzo":null,Borgsdorf:null,"Bork(Westf)":null,"Borken(Hess)":null,"Borken(Westf)":null,Borkheide:null,"Borna(Leipzig)":null,"Borne(NL)":null,"Bornholte(b Verl)":null,"Borsdorf(Hess)":null,"Borsdorf(Sachs)":null,Boskoop:null,"Boskoop Snijdelwijk":null,Bottighofen:null,"Bottrop Hbf":null,"Bottrop-Boy":null,"Bottrop-Vonderort":null,"Boulevarden st":null,"Bourg-St.Maurice":null,"Bourg-en-Bresse":null,Bourges:null,"Bous(Saar)":null,"Boven-Hardinxveld":null,"Bovenkarspel Flora":null,"Bovenkarspel-Grootebroek":null,"Boxberg-Wölchingen":null,Boxmeer:null,Boxtel:null,Brachbach:null,Brachelen:null,Brahlstorf:null,"Brake(Unterweser)":null,"Brake(b Bielefeld)":null,"Brakel(Höxter)":null,"Bramming st":null,Bramsche:null,"Bramstedt(b Syke)":null,"Brand Tropical Islands":null,"Brandenburg Altstadt":null,"Brandenburg Hbf":null,Brandoberndorf:null,"Brandstätt":null,Brannenburg:null,Brasov:null,"Bratislava hl.st.":null,"Bratislava-Petrzalka":null,Braubach:null,"Braunau/Inn":null,Braunsbedra:null,"Braunsbedra Ost":null,"Braunschweig Hbf":null,"Braunschweig-Gliesmarode":null,"Braunsdorf-Lichtenwalde":null,Brebach:null,Breclav:null,"Breclav(Gr)":null,Breda:null,"Breda-Prinsenbeek":null,Breddin:null,"Bredebro st":null,Bredelar:null,Bredenbek:null,Bredstedt:null,Brefeld:null,Bregenz:null,"Bregenz Hafen":null,"Bregenz Riedenburg":null,Brehna:null,"Breil-sur-Roya":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,"Brennero/Brenner":null,Brescia:null,"Bressanone/Brixen":null,Bressoux:null,"Brest(F)":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,Breukelen:null,Breyell:null,"Breziny u Decina":null,Brieselang:null,"Briesen(Mark)":null,Brig:null,"Brigachtal Kirchdorf":null,"Brigachtal Klengen":null,"Brilon Stadt":null,"Brilon Wald":null,Britz:null,"Brixen im Thale":null,Brixlegg:null,"Brno hl.n.":null,Brocken:null,"Brockhöfe":null,Broderstorf:null,"Broens st":null,Brohl:null,Brokstedt:null,Bronschhofen: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 Tunnelstraße":null,Bruchweiler:null,"Bruck-Fusch":null,"Bruck/Leitha":null,"Bruck/Mur":null,Bruckberg:null,Brucken:null,"Bruckmühl":null,"Brugg AG":null,Brugge:null,Brumath:null,Brummen:null,"Brunau-Packebusch":null,"Brunico/Bruneck":null,"Brunnen(CH)":null,"Brunnen(Oberbay)":null,"Brussels Airport - Zaventem":null,"Bruxelles Midi":null,"Bruxelles-Central":null,"Bruxelles-Luxembourg":null,"Bruxelles-Midi Eurostar":null,"Bruxelles-Nord":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ü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,"Buchs SG":null,"Buckow(Beeskow)":null,"Bucuresti Nord Gara A":null,"Budapest-Ferencváros":null,"Budapest-Keleti":null,"Budapest-Nyugati":null,Buddenhagen:null,Budenheim:null,Bufleben:null,Buggingen:null,Buir:null,Buitenpost:null,Buldern:null,"Bullay(DB)":null,"Bully-Grenay":null,Bunde:null,"Bundenthal-Rumbach":null,Bunnik: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,"Burgfried b.Gnas":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,"Buschmühle":null,Buschow:null,Busenbach:null,"Busenberg-Schindhard":null,Busigny:null,Bussnang:null,"Bussum Zuid":null,"Busto Arsizio":null,Buttenheim:null,"Buttstädt":null,Butzbach:null,Buxtehude:null,"Bydgoszcz Glowna":null,"Bäch":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öckstein":null,"Bödigheim":null,"Böheimkirchen":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önen-Nordbögge":null,"Bönningstedt":null,"Börnecke(Harz)":null,"Börßum":null,"Bösdorf(Sachs-Anh)":null,"Bösperde":null,"Bötzingen":null,"Bötzingen Mühle":null,"Bübingen":null,"Büchen":null,"Büchenbach":null,"Büches-Düdelsheim":null,"Büchig, Stutensee":null,"Bückeburg":null,"Büdingen(Oberhess)":null,"Büdingen(Westerw)":null,"Bühl(Baden)":null,"Bülach":null,"Bülzig":null,"Bünde(Westf)":null,"Bürgeln":null,"Bürgerhaus, Hessisch Lichtenau":null,"Bürglen":null,"Bürstadt":null,"Büsenbachtal":null,"Büsum":null,"Büttgen":null,"Bützow":null,Cadenazzo:null,Cadenberge:null,Cadolzburg:null,Cainsdorf:null,"Calais Ville":null,"Calais-Fréthun":null,"Calau(Nl)":null,"Calbe(Saale) Ost":null,"Calbe(Saale) Stadt":null,"Calbe(Saale) West":null,Calberlah:null,Caldern:null,"Caldes de Malavella":null,"Calmbach Bahnhof":null,"Calmbach Süd":null,Calw:null,"Camburg(Saale)":null,"Cammin(Meckl)":null,"Campo di Trens/Freienfeld":null,Cannes:null,"Cannes-la-Bocca":null,"Capelle Schollevaar":null,"Capelle(Westf)":null,Capellen:null,"Capolago-Riva S. Vitale":null,"Caputh Schwielowsee":null,"Caputh-Geltow":null,"Carbonne(Boussens)":null,Carcassonne:null,Carimate:null,"Carnoules(Toulon)":null,Casekow:null,"Casteldarne/Ehrenburg":null,Castelnaudary:null,"Castione-Arbedo":null,Castricum:null,"Castrop-Rauxel Hbf":null,"Castrop-Rauxel Süd":null,"Castrop-Rauxel-Merklinde":null,"Cavaillon(Avignon)":null,Celle:null,Centallo:null,"Cents-Hamm":null,"Cerbère":null,"Ceska Kamenice":null,"Ceska Kubice":null,"Ceska Lipa hl.n.":null,"Ceska Lipa strelnice":null,"Ceske Budejovice":null,"Ceske Velenice":null,"Chalon sur Saône":null,"Chalons en Champagne":null,"Cham(Oberpf)":null,"Chambery-Challes-E":null,Chamerau:null,Champigneulles:null,"Charleroi Sud":null,"Chateau-Thierry":null,"Chauny(Tergnier)":null,Cheb:null,"Cheb-Skalka":null,"Chelles Gournay":null,"Chemnitz Alt Chemnitz Center":null,"Chemnitz Annenstraße":null,"Chemnitz Bernsbachplatz":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 Küchwald":null,"Chemnitz Mitte":null,"Chemnitz Moritzhof":null,"Chemnitz Omnibusbahnhof":null,"Chemnitz Riemenschneiderstraße":null,"Chemnitz Rosenbergstraß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 Stadlerplatz":null,"Chemnitz Süd":null,"Chemnitz TU Campus":null,"Chemnitz Technopark":null,"Chemnitz Theaterplatz":null,"Chemnitz Treffurthstraße":null,"Chemnitz Uhlestraße":null,"Chemnitz Zentralhaltestelle":null,"Chemnitz-Altchemnitz":null,"Chemnitz-Borna Hp":null,"Chemnitz-Harthau":null,"Chemnitz-Hilbersdorf":null,"Chemnitz-Reichenhain":null,"Chemnitz-Schönau":null,"Chemnitz-Siegmar":null,"Chenay Gagny":null,Chenee:null,Cherbourg:null,"Chevremont(NL)":null,Chiasso:null,"Chiusa/Klausen":null,Chomutov:null,"Chomutov mesto":null,Chorin:null,Chotyne:null,Chrastava:null,"Chrastava-Andelska Hora":null,Chribska:null,Chur:null,"Château du Loir":null,"Château-Arnoux-St-Auban":null,"Châteauroux":null,"Châtelet":null,Cintegabelle:null,Clarholz:null,Clausnitz:null,"Clermont-Ferrand":null,Clerval:null,Clervaux:null,Cloppenburg:null,Coburg:null,"Coburg Nord":null,"Coburg-Beiersdorf":null,"Coburg-Neuses":null,"Cochem(Mosel)":null,"Coesfeld Schulzentrum":null,"Coesfeld(Westf)":null,Coevorden:null,"Colle Isarco/Gossensass":null,Collenberg:null,Collioure:null,Colmar:null,"Combs la Ville Quincy":null,"Como S. Giovanni":null,Compiegne:null,Conegliano:null,"Conflans-Jarny":null,Contwig:null,"Coppenbrügge":null,"Corbehem(Douai)":null,"Corbeil Essonnes":null,Cornaux:null,Coschen:null,Cosne:null,Cossebaude:null,"Cossonay-Penthalaz":null,"Coswig(Anh)":null,"Coswig(b Dresden)":null,"Cottbus Hbf":null,"Cottbus-Merzdorf":null,"Cottbus-Sandow":null,"Cottbus-Willmersdorf Nord":null,Coulommiers:null,"Courcelles-sur-Nied":null,Coutras:null,Crailsheim:null,Cranzahl:null,Creidlitz:null,Creil:null,"Creußen(Oberfr)":null,Crimmitschau:null,Crivitz:null,"Crossen Ort":null,"Crossen a d Elster":null,Cuijk:null,Culemborg:null,"Culmont-Chalindrey":null,Culoz:null,Cuneo:null,Cunnertswalde:null,Cursdorf:null,Curtici:null,Cuxhaven:null,Czerwiensk:null,"Cölbe":null,Daaden:null,Daarlerveen:null,Dabendorf:null,"Dachau Bahnhof":null,"Dachau Stadt":null,Dachrieden:null,Dachsen:null,Dachwig:null,"Dagebüll Kirche":null,"Dagebüll Mole":null,Dagmersellen:null,Dahl:null,Dahlbruch:null,"Dahlem(Eifel)":null,"Dahlen(Sachs)":null,Dahlenburg:null,"Dahlerbrück":null,Dahlewitz:null,Dahn:null,"Dahn Süd":null,"Dalen(NL)":null,Dalfsen:null,Dalheim:null,Dallau:null,"Dallgow-Döberitz":null,"Dambeck(Altm)":null,"Dammerstock, Karlsruhe":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,"Davos Dorf":null,"Davos Platz":null,Dax:null,"Daxlanden Dornröschenweg, Karlsruhe":null,"Daxlanden Karl-Delisle-Straße, Karlsruhe":null,"Daxlanden Nussbaumweg, Karlsruhe":null,"Daxlanden Thomas-Mann-Straße, Karlsruhe":null,"De Vink":null,Debrecen:null,"Decin hl.n.":null,"Decin vychod":null,"Decin-Certova voda":null,"Decin-Priper":null,"Decin-Prostredni Zleb":null,Dedenhausen:null,"Dedensen-Gümmer":null,Dedinghausen:null,"Deezbüll":null,"Deggendorf Hbf":null,Deidesheim:null,"Deining(Oberpf)":null,Deinste:null,Deinum:null,Deisenhofen:null,"Deißlingen Mitte":null,Delden:null,Delft:null,"Delft Campus":null,Delfzijl:null,"Delfzijl West":null,"Delitzsch ob Bf":null,"Delitzsch unt Bf":null,Dellfeld:null,"Dellfeld Ort":null,Delmenhorst:null,"Delmenhorst Hasporter Damm":null,"Delémont":null,"Demitz-Thumitz":null,Demker:null,Demmin:null,"Den Dolder":null,"Den Haag Centraal":null,"Den Haag HS":null,"Den Haag Laan van Nieuw Oost Indie":null,"Den Haag Mariahoeve":null,"Den Haag Moerwijk":null,"Den Haag Ypenburg":null,"Den Helder":null,"Den Helder Zuid":null,Denderleeuw:null,Densborn:null,Denzlingen:null,Dernau:null,"Dernbach(Westerw)":null,"Derneburg(Han)":null,Desenice:null,"Desenzano del Garda/Sirmione":null,Desio: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,Dettwiller:null,"Deuben(Zeitz)":null,Deuerling:null,Deurne:null,Deuten:null,Deutzen:null,Deva:null,Deventer:null,"Deventer Colmschate":null,"Devinska Nova Ves":null,Didam:null,Diebach:null,Dieburg:null,Diedelsheim:null,"Diedorf(Schwab)":null,Diemen:null,"Diemen Zuid":null,Diemeringen:null,Dienheim:null,Diepenbeek:null,Diepholz:null,Dieren:null,Dieskau:null,Diessenhofen:null,"Diessenhofen URh":null,Dietersheim:null,Dietlikon:null,Dietmannsried:null,Dietzelbach:null,"Dietzenbach Bahnhof":null,"Dietzenbach Mitte":null,"Dietzenbach-Steinberg":null,Dietzhausen:null,Dieulouard:null,Diez:null,"Diez Ost":null,"Dießen":null,Differdange:null,"Dijon Porte Neuve":null,"Dijon Ville":null,Dillbrecht:null,Dillenburg:null,"Dillingen(Donau)":null,"Dillingen(Saar)":null,Dingolfing:null,"Dinkelsbühl Bf":null,Dinkelscherben:null,Dinslaken:null,"Dippach-Reckange":null,Dippoldiswalde:null,Dirmingen:null,"Dissen-Bad Rothenfelde":null,Distelhausen:null,Ditfurt:null,Dittersbach:null,Dittersdorf:null,Dittigheim:null,Ditzingen:null,"Dobbiaco/Toblach":null,"Doberlug-Kirchhain":null,"Doberschütz":null,Dobova:null,"Dobova(Gr)":null,Dodendorf:null,Dodenhof:null,"Doestrup(Soenderjylland) st":null,Doetinchem:null,"Doetinchem De Huet":null,Dogern:null,"Dohna(Sachs)":null,Doksy:null,"Dole Ville":null,"Dolhain-Gileppe":null,Dollbergen:null,Dollern:null,Dollnstein:null,"Dolni Habartice":null,"Dolni Podluzi":null,"Dolni Poustevna":null,"Dolni Zleb":null,"Dolni Zleb zast.":null,Domazlice:null,"Dombühl":null,Dommeldange:null,"Domnitz(Saalkr)":null,Domodossola:null,"Domsühl":null,Donaueschingen:null,"Donaueschingen Allmendshofen":null,"Donaueschingen Aufen":null,"Donaueschingen Grüningen":null,"Donaueschingen Mitte/Siedlung":null,"Donauwörth":null,Dordrecht:null,"Dordrecht Stadspolders":null,"Dordrecht Zuid":null,"Dorf Mecklenburg":null,Dorfchemnitz:null,"Dorfen Bahnhof":null,Dorfgastein:null,Dorfmark:null,Dorfprozelten:null,"Dorheim(Wetterau)":null,Dormagen:null,"Dormagen Chempark":null,Dornbirn:null,"Dornbirn Schoren":null,"Dornburg(Saale)":null,Dornstetten:null,"Dornstetten-Aach":null,Dorsten:null,Dortelweil: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 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,"Dottikon-Dintikon":null,Dourges:null,Drahnsdorf:null,Drahtzug:null,Drauffelt:null,Drebkau:null,"Drei Annen Hohne":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,"Driebergen-Zeist":null,Driehuis:null,"Drohndorf-Mehringen":null,Dronryp:null,Dronten:null,Duchcov:null,Ducherow:null,Duckterath:null,Dudweiler:null,"Dugo Selo":null,"Duisburg Entenfang":null,"Duisburg Hbf":null,"Duisburg-Buchholz":null,"Duisburg-Großenbaum":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,Duiven:null,Duivendrecht:null,Dunkerque:null,Durach:null,"Durlach Hubstraße, Karlsruhe":null,"Durlach Untermühlstraße, Karlsruhe":null,"Durlacher Tor/KIT-Campus Süd, Karlsruhe":null,Durmersheim:null,"Durmersheim Nord":null,"Dutenhofen(Wetzlar)":null,"Dußlingen":null,"Dyreby st":null,"Däniken":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öttingen":null,"Dülken":null,"Dülmen":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ürrnhaar":null,"Dürrröhrsdorf":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-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,"Eben im Pongau":null,Ebenfurth: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,Ebersheim:null,"Eberswalde Hbf":null,Ebertsheim:null,Ebing:null,Ebringen:null,"Ebstorf(Uelzen)":null,Echem:null,Eching:null,Echt:null,Echterdingen:null,Echzell:null,Eckardtsleben:null,"Eckartsberga(Thür)":null,"Eckartshausen-Ilshofen":null,"Eckenerstraße, Karlsruhe":null,"Eckernförde":null,"Eckersmühlen":null,Eddersheim:null,"Ede Centrum":null,"Ede(B)":null,"Ede-Wageningen":null,Edelfingen:null,Edenkoben:null,"Edermünde-Grifte":null,"Edesheim(Pfalz)":null,"Ediger-Eller":null,"Edingen(Wetzlar)":null,"Edle Krone":null,Edling:null,Eemshaven:null,"Effelder(Thür)":null,Effolderbach:null,Effretikon:null,"Efringen-Kirchen":null,Egelsbach:null,Egersdorf:null,"Egestorf(Deister)":null,Eggenfelden:null,"Eggenfelden Mitte":null,"Eggenstein Bf":null,"Eggenstein Schweriner Straße, Eggenstein-Leopoldsh":null,"Eggenstein Spöcker Weg, Eggenstein-Leopoldshafen":null,"Eggenstein Süd, Eggenstein-Leopoldshafen":null,Eggersdorf:null,Eggesin:null,Eggingen:null,Egglkofen:null,"Eggmühl":null,Eggolsheim:null,Eglharting:null,Egling:null,Eglisau:null,Egnach: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,"Ehrwald Zugspitzbahn":null,Eibau:null,"Eich(Sachs)":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,Eijsden:null,Eilenburg:null,"Eilenburg Ost":null,Eilendorf:null,"Eilsleben(b Magdeburg)":null,Eilvese:null,Eimeldingen:null,"Einbeck Mitte":null,"Einbeck Otto-Hahn-Straße":null,"Einbeck-Salzderhelden":null,"Eindhoven Centraal":null,"Eindhoven Strijp-S":null,Einfeld:null,Einsiedel:null,"Einsiedel Brauerei":null,"Einsiedel Hp Gymnasium":null,Einsiedeln:null,Einsiedlerhof:null,"Einöd(Saar)":null,Eisemroth:null,Eisenach:null,"Eisenach Opelwerke Hp":null,"Eisenach West":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,Elne:null,Elpersheim:null,Elsbethen:null,Elsfleth:null,Elsholz:null,"Elsnigk(Anh)":null,Elst: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,"Emmen Zuid":null,"Emmen(NL)":null,"Emmenbrücke":null,Emmendingen:null,Emmerich:null,"Emmerich-Elten":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,Engis:null,Engstingen:null,"Engstingen Schulzentrum":null,Engstlatt:null,Enkenbach:null,Enkhuizen:null,Ennepetal:null,Enns:null,Enschede:null,"Enschede De Eschmarke":null,"Enschede Kennispark":null,"Ensdorf(Saar)":null,Enspel:null,"Entenfang, Karlsruhe":null,Entringen:null,Enzberg:null,Enzisweiler:null,"Epe(Westf)":null,Epernay:null,"Epierre-St Leger":null,Epinal: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,"Erbprinz/Schloss, Ettlingen":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,"Erkner (S)":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,Erlen:null,"Erlenbach(Main)":null,Ermatingen:null,"Ermatingen URh":null,Ermelo:null,"Erndtebrück":null,Ernsgaden:null,Ernsthausen:null,"Ernstthal am Rennsteig":null,"Erpel(Rhein)":null,Erpolzheim:null,Erquelinnes:null,Ersingen:null,"Ersingen West":null,Erstein:null,Erstfeld:null,Erzhausen:null,"Erzingen(Baden)":null,"Erzingen(Württ)":null,"Esbjerg st":null,"Esch-sur-Alzette":null,Eschborn:null,"Eschborn Süd":null,Eschede:null,Eschelbronn:null,"Eschenau(Mittelfr)":null,"Eschenau(b Heilbronn)":null,"Eschenau/Salzach":null,"Eschenbach(b Markt Erlbach)":null,Eschenlohe:null,"Escherndorf-Vogelsburg":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,"Eslöv station":null,Espelkamp:null,"Espenau-Mönchehof":null,Essel:null,"Essen Hbf":null,"Essen Stadtwald":null,"Essen Süd":null,"Essen West":null,"Essen(B)":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,"Essenweinstraße, Karlsruhe":null,"Esslingen(Neckar)":null,"Esslingen-Mettingen":null,"Esslingen-Zell":null,Esting:null,Etampes:null,Etelsen:null,Ettelbruck:null,"Etten-Leur":null,Ettenhausen:null,Etterzhausen:null,"Ettlingen Stadt":null,"Ettlingen West":null,Etzbach:null,Etzelwang:null,Etzenbach:null,Etzenricht:null,Etzenrot:null,Etzleben:null,Etzwilen:null,Eubigheim:null,Euerdorf:null,Eupen:null,"Europaplatz/Postgal. (Kaiser), Karlsruhe":null,Euskirchen:null,"Euskirchen Zuckerfabrik":null,"Euskirchen-Großbüllesheim":null,"Euskirchen-Kreuzweingarten":null,"Euskirchen-Kuchenheim":null,"Euskirchen-Stotzheim":null,Eutin:null,"Eutingen Nord":null,"Eutingen im Gäu":null,"Eutingen(Baden)":null,"Evreux Normandie":null,Eyach:null,Eygelshoven:null,"Eygelshoven Markt":null,Eystrup:null,"Eßleben":null,"Faak am See":null,"Fachingen(Lahn)":null,Fahrenkrug:null,Fahrnau:null,Faido:null,"Falkenau(Sachs)Hp":null,"Falkenau(Sachs)Süd":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,Faulquemont:null,Faurndau:null,Favoritepark:null,Feanwalden:null,"Fegersheim Lipsheim":null,"Fehmarn-Burg":null,Fehraltorf:null,Fehring:null,Feilitzsch:null,Feldafing:null,"Feldbach/Raab":null,"Feldberg-Bärental":null,Felde:null,Feldhausen:null,Feldkirch:null,"Feldkirch Amberg":null,"Feldkirchen in Kärnten":null,"Feldkirchen(b München)":null,Feldolling:null,Felixdorf:null,Fellbach:null,"Felsberg-Altenbrunslar":null,"Felsberg-Gensungen":null,"Felsberg-Wolfershausen":null,"Ferch-Lienewitz":null,Ferdinandshof:null,Fermerswalde:null,"Ferndorf(Siegen)":null,Ferrara:null,Feucht:null,"Feucht Ost":null,"Feucht-Moosbach":null,Feudingen:null,Fichtenberg:null,Fieberbrunn:null,Figueres:null,Filderstadt:null,Filisur:null,Filsen:null,Finkenheerd:null,Finkenkrug:null,Finnentrop:null,"Finningerstraße":null,Finsterwald:null,"Finsterwalde(Niederlausitz)":null,"Firenze S.M.N.":null,"Fischbach(Nürnberg)":null,"Fischbach-Camphausen":null,"Fischbach-Weierbach":null,Fischbachau:null,Fischen:null,Fischhaus:null,"Fischhausen-Neuhaus":null,"Fischweier, Karlsbad":null,Flamatt:null,Flassa:null,Flaurling:null,Flechtingen:null,Fleetmark:null,Flehingen:null,Flensburg:null,"Flensburg-Weiche":null,Flers:null,Flieden:null,Flintbek:null,Flintsbach:null,Flomersheim:null,"Floßmühle":null,"Flughafen BER - Terminal 1-2":null,"Flughafen BER - Terminal 1-2 (S-Bahn)":null,"Flughafen BER - Terminal 5 (Schönefeld)":null,"Flughafen Wien":null,Flums:null,"Flöha":null,"Flöha-Plaue":null,"Flörsheim(Main)":null,"Flüelen":null,Fohrde:null,Fontaine:null,"Fontainebleau-Avon":null,"Fontan Saorge":null,"Forbach(F)":null,"Forbach(Schwarzw)":null,"Forchheim Hallenbad, Rheinstetten":null,"Forchheim Hauptstraße, Rheinstetten":null,"Forchheim Leichtsandstr./Messe Karlsruhe, Rheinste":null,"Forchheim Leichtsandstraße/Messe Karlsruhe, Rheins":null,"Forchheim Oberfeldstraße, Rheinstetten":null,"Forchheim(Oberfr)":null,"Forchheim(b Karlsruhe)":null,"Forest Midi/Vorst Zuid":null,Fornsbach:null,"Forst(Lausitz)":null,"Forstfeldstraße, Kassel":null,Forsthaus:null,Forsting:null,"Fortezza/Franzensfeste":null,Forth:null,Fossano:null,Fourchambault:null,Frahelsbruck:null,Fraipont:null,Franeker:null,"Frankenberg(Eder)":null,"Frankenberg(Sachs)":null,"Frankenberg(Sachs) Süd":null,"Frankenberg-Goßberg":null,"Frankenberg-Viermünden":null,Frankenmarkt: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) Stresemannallee/Mörfelder Landstr":null,"Frankfurt(Main)-Gateway Gardens":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-Mainkur":null,"Frankfurt-Nied":null,"Frankfurt-Niederrad":null,"Frankfurt-Rödelheim":null,"Frankfurt-Sindlingen":null,"Frankfurt-Sossenheim":null,"Frankfurt-Unterliederbach":null,"Frankfurt-Zeilsheim":null,Frankleben:null,"Frantiskovy Lazne":null,"Frantiskovy Lazne Aquaforum":null,Frastanz:null,"Frauenalb-Schielberg":null,Frauenau:null,Frauenfeld:null,Frauenhain:null,"Frechen-Königsdorf":null,Freckleben:null,"Freden(Leine)":null,Fredenbeck:null,"Fredericia st":null,"Fredersdorf(b Berlin)":null,"Freiberg(Neckar)":null,"Freiberg(Sachs)":null,"Freiburg Klinikum":null,"Freiburg Messe/Universität":null,"Freiburg(Breisgau) Hbf":null,"Freiburg-Herdern":null,"Freiburg-Landwasser":null,"Freiburg-Littenweiler":null,"Freiburg-St Georgen":null,"Freiburg-Wiehre":null,"Freiburg-Zähringen":null,"Freienbach SBB":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 West":null,"Freital-Potschappel":null,Frellstedt:null,Frelsdorf:null,"Fremdingen Bf":null,Fremersdorf:null,"Frenkendorf-Füllinsdorf":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,"Fribourg/Freiburg":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,"Friedrichsplatz, Kassel":null,"Friedrichsruhe(Meck)":null,Friedrichssegen:null,Friedrichstadt:null,"Friedrichstal Mitte, Stutensee":null,"Friedrichstal Nord, Stutensee":null,"Friedrichstal Saint-Riquier-Platz, Stutensee":null,"Friedrichstal b Freudenstadt":null,"Friedrichstal(Baden)":null,"Friedrichsthal(Saar)":null,"Friedrichsthal(Saar) Mitte":null,"Friedrichsthal(b Bayreuth)":null,"Friedrichswalde(bei Eberswalde)":null,"Friesach in Kärnten":null,"Friesack(Mark)":null,Friesdorf:null,"Friesdorf Ost":null,"Friesenheim(Baden)":null,Frimmersdorf:null,"Frisvadvej st":null,"Fritzens-Wattens":null,Fritzlar:null,Frohburg:null,Frommern:null,"Fronhausen(Lahn)":null,Frontenex:null,Frose:null,Frouard:null,Frutigen:null,"Frömern":null,"Fröndenberg":null,"Fröttstädt":null,Fulda:null,"Fuldatal-Ihringshausen":null,Furschenbach:null,"Furth im Wald":null,"Furth(b Deisenhofen)":null,Futuroscope:null,"Fährbrücke":null,"Föderlach":null,"Föhren":null,"Förbau":null,"Förderstedt":null,"Förtha(Eisenach)":null,"Förtschendorf":null,"Fürfurt":null,"Fürnitz":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-Unterfürberg":null,"Füssen":null,Gaanderen:null,Gablingen:null,Gadebusch:null,"Gaggenau Bf":null,"Gaggenau Mercedes-Benz Werk":null,Gagny:null,"Gaildorf West":null,"Gaillon Aubevoye":null,Gaimersheim:null,"Gaißach":null,Gallarate:null,"Gamburg(Tauber)":null,Gammertingen:null,"Gammertingen Europastraße":null,"Gampel-Steg":null,Ganderkesee:null,"Gandrange-Amneville":null,"Gangloffsömmern":null,Gannat:null,Ganzlin:null,Garbeck:null,Garbenteich:null,"Garching(Alz)":null,Gardanne: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,"Gdansk Glowny":null,"Gdansk Oliwa":null,"Gdansk Wrzeszcz":null,"Gdynia Glowna":null,Gebersdorf:null,"Gebra(Hainleite)":null,Geeste:null,Geestenseth:null,Geestgottberg:null,Gehlberg:null,Geigant:null,Geilenkirchen:null,Geilhausen:null,Geinberg: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,Geldermalsen:null,Geldern:null,Geldrop:null,"Geleen Oost":null,"Geleen-Lutterade":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,Gelterkinden:null,Gemmingen:null,"Gemmingen West":null,"Gemona del Friuli":null,"Gemünden(Main)":null,Genderkingen:null,Gendorf:null,Gengenbach:null,Genk:null,Gennweiler:null,"Genova Piazza Principe":null,"Gensingen-Horrweiler":null,"Gent St Pieters":null,"Gent-Dampoort":null,Gentbrugge:null,Genthin:null,"Genève":null,"Genève-Aéroport":null,"Georgensgmünd":null,"Gera Hbf":null,"Gera Süd":null,"Gera-Langenberg":null,"Gera-Zwötzen":null,Geraberg:null,Geradstetten:null,Gerhausen:null,Gerichshain:null,Gerlachsheim:null,Gerlafingen:null,Gerlenhofen:null,"Gerling im Pinzgau":null,"Germering-Unterpfaffenhofen":null,Germersheim: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,"Gevrey-Chambertin":null,"Giengen(Brenz)":null,Giersleben:null,"Gießen":null,"Gießen Erdkauter Weg":null,"Gießen Licher Str":null,"Gießen Oswaldsgarten":null,"Gießenbach in Tirol":null,Gifhorn:null,"Gifhorn Stadt":null,"Gilching-Argelsried":null,Gildenhall:null,"Gilze-Rijen":null,"Gingen(Fils)":null,Girod:null,Girona:null,"Gisikon-Root":null,"Gisors Embranchement":null,"Gittelde/Bad Grund(Harz)":null,Giubiasco:null,"Gjesing st":null,"Gladbeck Ost":null,"Gladbeck West":null,"Gladbeck-Zweckel":null,"Glan-Münchweiler":null,Glanerbrug:null,Glanzstoffwerke:null,"Glashütte(Sachs)":null,Glattbrugg:null,Glattfelden:null,"Glaubitz(Riesa)":null,"Glauburg-Glauberg":null,"Glauburg-Stockheim":null,"Glauchau(Sachs)":null,"Glauchau-Schönbörnchen":null,Gleisdorf:null,Glesch:null,Gloggnitz:null,"Glossen(b Oschatz)":null,"Glöwen":null,"Glückauf":null,"Glückstadt":null,"Gmund(Tegernsee)":null,"Gmünd NÖ":null,Gnadau:null,Gnarrenburg:null,"Gnarrenburg Nord":null,Gnevkow:null,Gniezno:null,Goch:null,"Gochsheim(Baden)":null,Godramstein:null,"Goebelsmühle":null,Goes:null,Gokels:null,"Goldbeck(Osterburg)":null,"Goldberg(Württ)":null,"Goldenstedt(Oldb)":null,Goldhausen:null,"Goldshöfe":null,"Golling-Abtenau":null,"Gollmitz(Niederlausitz)":null,Golm:null,"Golzow(Eberswalde)":null,"Golzow(Oderbruch)":null,"Golßen(Niederlausitz)":null,Gomadingen:null,Gommern:null,"Gondelsheim Schlossstadion":null,"Gondelsheim(Baden)":null,Goor:null,Goppenstein:null,Gorgast:null,Gorinchem:null,Gosberg:null,Goslar:null,"Gossau SG":null,Gotha:null,"Gotha Ost":null,Gottenheim:null,"Gottesauer Platz/BGV, Karlsruhe":null,Gotteszell:null,"Gottlieben (Schifflände)":null,Gottmadingen:null,Gouda:null,"Gouda Goverwelle":null,Gouvy: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,Graffenstaden:null,"Grafing Bahnhof":null,"Grafing Stadt":null,"Grafling-Arzting":null,Grafrath:null,Gramatneusiedl:null,Grambow:null,Gramsbergen:null,Granollers:null,Gransee:null,"Gratwein-Gratkorn":null,"Grauschwitz Flocke":null,"Graz Hbf":null,"Graz Ostbahnhof-Messe":null,Grebenstein:null,"Gredstedbro st":null,Greifswald:null,"Greifswald Süd":null,Greiz:null,"Greiz-Dölau":null,"Grenchen Nord":null,Grenoble:null,Grenzach:null,Greppin:null,"Gresy-sur-Isere":null,"Gretz-Armainvilliers":null,"Greußen":null,Greven:null,Grevenbroich:null,"Grevesmühlen":null,"Grieben(Meckl)":null,Griebo:null,Griefstedt:null,"Gries am Brenner":null,"Gries im Pinzgau":null,"Griesen(Oberbay)":null,"Grieskirchen-Gallspach":null,"Grießen(Baden)":null,Grijpskerk:null,"Grimma ob Bf":null,Grimmen:null,Grimmenthal:null,Grobau:null,Groenendaal:null,Grombach:null,"Gronau(Westf)":null,Groningen:null,"Groningen Europapark":null,"Groningen Noord":null,Gronsdorf:null,"Grou-Jirnsum":null,"Groß Ammensleben":null,"Groß Behnitz":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ßbodungen":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,Grunbach:null,"Grunow(Niederlausitz)":null,"Gräfelfing":null,"Gräfenberg":null,"Gräfendorf":null,"Gräfenhainichen":null,"Gräfenroda":null,"Gräfenstuhl-Klippmühle":null,"Gräfentonna":null,"Gräveneck":null,"Grävenwiesbach":null,"Gröbenzell":null,"Gröbers":null,"Gröbming":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,"Guldager st":null,Gummersbach:null,"Gummersbach-Dieringhausen":null,"Gumpenried-Asbach":null,"Gundelfingen(Bay)":null,"Gundelfingen(Breisgau)":null,Gundelsdorf:null,Gundelshausen:null,"Gundelsheim(Neckar)":null,"Gundersheim(Rheinhess)":null,Guntersblum:null,"Guntramsdorf Kaiserau":null,Gunzenhausen:null,"Gurten OÖ":null,Gussenstadt:null,Gustorf:null,"Gutach Freilichtmuseum":null,"Gutach(Breisgau)":null,"Gutenfürst":null,Guthmannshausen:null,Guxhagen:null,"Györ":null,"Gänserndorf":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öschenen":null,"Götschendorf":null,"Göttingen":null,"Götz":null,"Götzendorf/Leitha":null,"Götzis":null,"Gößnitz":null,"Güdingen":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ütersloh Hbf":null,"Güttingen":null,Haaltert:null,Haan:null,"Haan-Gruiten":null,Haar:null,Haarhausen:null,Haarlem:null,"Haarlem Spaarnwoude":null,"Habsheim(Mulh)":null,Hachenburg:null,Hadamar:null,Hademarschen:null,Hademstorf:null,Hadmersleben:null,Haffkrug:null,"Hagebök":null,Hagelstadt:null,"Hagen Hbf":null,"Hagen(Han)":null,"Hagen(Kr. Stade)":null,"Hagen-Heubing":null,"Hagen-Oberhagen":null,"Hagen-Vorhalle":null,"Hagen-Wehringhausen":null,"Hagen-Westerbauer":null,"Hagenau im Innkreis":null,Hagenbach:null,"Hagenbüchach":null,"Hagenow Land":null,"Hagenow Stadt":null,Hagenwerder:null,Hagondange:null,"Hagsfeld Bahnhof, Karlsruhe":null,"Hagsfeld Geroldsäcker, Karlsruhe":null,"Hagsfeld Jenaer Straße, Karlsruhe":null,"Hagsfeld Reitschulschlag (Schleife), Karlsruhe":null,"Hagsfeld Reitschulschlag, Karlsruhe":null,"Hagsfeld Süd, Karlsruhe":null,Haguenau:null,"Haidenaab-Göppmannsbühl":null,Haidkapelle:null,Haiger:null,"Haiger Obertor":null,Haigerloch:null,"Hailer-Meerholz":null,Haiming: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,"Halfweg-Zwanenburg":null,"Halitplatz, Kassel":null,"Hall in Tirol":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(S) Heidebf":null,"Halle(Saale)Hbf":null,"Halle(Westf)":null,"Halle(Westf) OWL-Arena":null,"Halle-Ammendorf":null,"Halle-Neustadt":null,"Halle-Nietleben":null,"Halle-Silberhöhe":null,"Halle-Trotha":null,Hallein:null,"Hallstadt(b Bamberg)":null,"Hallwang-Elixhausen":null,Halstenbek:null,"Haltern am See":null,Haltingen:null,"Halver-Oberbrügge":null,"Hamburg Airport":null,"Hamburg Alte Wöhr":null,"Hamburg Berliner Tor":null,"Hamburg Billwerder-Moorfleet":null,"Hamburg Burgwedel":null,"Hamburg Dammtor":null,"Hamburg Diebsteich":null,"Hamburg Elbbrücken":null,"Hamburg Elbgaustraße":null,"Hamburg Friedrichsberg":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 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-Blankenese":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-Ohlsdorf":null,"Hamburg-Othmarschen":null,"Hamburg-Poppenbüttel":null,"Hamburg-Rahlstedt":null,"Hamburg-Rissen":null,"Hamburg-Rothenburgsort":null,"Hamburg-Schnelsen":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)Hbf":null,"Hamm-Bockum-Hövel":null,"Hamm-Heessen":null,Hammah:null,Hammelburg:null,"Hammelburg Ost":null,Hammelspring:null,Hammerau:null,"Hammersbach Zugspitzbahn, Grainau":null,Hammerstein:null,Hammerunterwiesenthal:null,Hamminkeln:null,"Hamminkeln-Dingden":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,"Hanweiler-Bad Rilchingen":null,Happurg:null,Harblek:null,"Harburg(Schwab)":null,"Hard-Fussach":null,Hardegsen:null,Hardenberg:null,Harderwijk:null,Hardhof:null,"Hardinxveld Blauwe Zoom":null,"Hardinxveld-Giessendam":null,"Haren(Ems)":null,"Haren(NL)":null,Harlesiel:null,"Harlingen(NL)":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,Harzgerode:null,Hasbergen:null,Haselbrunn:null,"Haselstauden (Dornbirn)":null,Haslach:null,"Hasloch(Main)":null,Hasloh:null,Haslohfurth:null,Haspelmoor:null,"Hassel(Saar)":null,Hasselborn:null,Hasselfelde:null,Hasselt:null,Haste:null,"Hatlerdorf(Dornbirn)":null,Hattenheim:null,"Hattersheim(Main)":null,Hattert:null,"Hatting in Tirol":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,"Hauptfriedhof, Karlsruhe":null,"Hauptfriedhof, Kassel":null,Hauptstuhl:null,Hauptwil:null,"Haus Bethlehem, Karlsruhe":null,"Haus im Ennstal":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,Hayange:null,Haynsburg:null,Hazebrouck: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,Heemskerk:null,"Heemstede-Aerdenhout":null,Heerbrugg:null,Heerenveen:null,Heerhugowaard:null,Heerlen:null,"Heerlen Woonboulevard":null,Heeze:null,"Hegelsbergstraße, Kassel":null,Heggen:null,Hegne:null,Hegyeshalom:null,"Hegyeshalom(Gr)":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 Karlstor":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,Heiloo:null,Heilsbronn:null,"Heimbach (Eifel)":null,"Heimbach(Nahe)":null,"Heimbach(Nahe)Ort":null,Heimenkirch:null,Heimerdingen:null,Heimersheim:null,Heimstetten:null,Heinebach:null,Heino:null,"Heinrich-Heine-Straße, Kassel":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,"Heiterwang-Plansee":null,Heldrungen:null,Helenesee:null,Helmbrechts:null,Helmond:null,"Helmond Brandevoort":null,"Helmond Brouwhuis":null,"Helmond t Hout":null,"Helmsdorf(Pirna)":null,Helmsheim:null,"Helmstadt(Baden)":null,Helmstedt:null,Helpup:null,"Hemmen-Dodewaard":null,Hemmerde:null,"Hemmersdorf(Saar)":null,Hemmingen:null,Hemmoor:null,Hemsbach:null,"Hemsen(b Soltau)":null,Hendaye:null,Hendschiken:null,Henfenfeld:null,Hengelo:null,"Hengelo Gezondheidspark":null,"Hengelo Oost":null,"Henin-Beaumont":null,"Henne st":null,"Hennef im Siegbogen":null,"Hennef(Sieg)":null,Hennen:null,"Hennersdorf(Sachs)":null,"Hennigsdorf (S)":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,Herentals:null,Herford:null,Hergatz:null,Hergenrath: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,Herny:null,Heroldsberg:null,"Heroldsberg Nord":null,Herrath:null,Herrenberg:null,"Herrenberg Zwerchweg":null,Herrensee:null,"Herrenstraße, Karlsruhe":null,Herrlingen:null,"Herrlisheim près Colmar":null,"Herrlishöfen":null,Herrnburg:null,Herrsching:null,"Hersbruck(l Pegnitz)":null,"Hersbruck(r Pegnitz)":null,Herstal: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,Herzele:null,Herzhorn:null,Herzogenbuchsee:null,Herzogenburg: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,"Hettange Grande":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,"Hildbrandsgrün":null,Hildburghausen:null,Hilden:null,"Hilden Süd":null,"Hildesheim Hbf":null,"Hildesheim Ost":null,Hillegom:null,"Hillnhütten":null,Hilpertsau:null,Hilpoltstein:null,Hilter:null,Hilversum:null,"Hilversum Media Park":null,"Hilversum Sportpark":null,Himmelpforten:null,Himmelreich:null,Himmelstadt:null,Hindeloopen: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,"Hirtenweg/Technologiepark, Karlsruhe":null,Hittfeld:null,Hitzacker:null,Hnevice:null,Hochdahl:null,"Hochdahl-Millrath":null,"Hochdorf(b Horb)":null,Hochfelden:null,Hochfilzen:null,"Hochhausen(Tauber)":null,"Hochheim(Main)":null,Hochneukirch:null,Hochspeyer:null,"Hochstadt-Marktzeuln":null,Hochstetten:null,"Hochstetten Altenheim, Linkenheim-Hochstetten":null,"Hochstetten Grenzstraße":null,"Hochstetten(Nahe)":null,"Hochstätten(Pfalz)":null,Hochwang:null,Hochzirl:null,Hockenheim:null,Hockeroda:null,Hodenhagen:null,Hodonin:null,Hoeilaart:null,"Hoeje Taastrup st":null,Hoensbroek:null,Hoevelaken: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,Hohenau:null,Hohenbrunn:null,Hohendorf:null,"Hohenebra Ort":null,Hoheneggelsen:null,Hohenems: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,"Hollandsche Rading":null,"Holländische Straße, Kassel":null,"Holländischer Platz/Universität, Kassel":null,"Holm-Seppensen":null,Holstentherme:null,Holten:null,"Holtensen/Linderte":null,Holthusen:null,"Holzdorf(Elster)":null,"Holzdorf(b Weimar)":null,"Holzgerlingen Bf":null,"Holzgerlingen Buch":null,"Holzgerlingen Hülben":null,Holzhau:null,"Holzhau Skilift":null,"Holzhausen(Kr Siegen)":null,"Holzheim(b Neuss)":null,Holzkirchen:null,Holzminden:null,Holzwickede:null,"Hombourg-Haut":null,"Homburg(Saar)Hbf":null,Honrath:null,Hoofddorp:null,Hoogeveen:null,"Hoogezand-Sappemeer":null,Hoogkarspel:null,Hoorn:null,"Hoorn Kersenboogerd":null,"Hopfgarten im Brixental":null,"Hopfgarten im Brixental Berglift":null,"Hopfgarten(Sachs)":null,"Hopfgarten(Weimar)":null,Hoppecke:null,"Hoppegarten(Mark)":null,Hoppingen:null,"Hoppstädten(Nahe)":null,Horb:null,"Horb-Heiligenfeld":null,Horgen:null,Horka:null,"Horn(Bodensee)":null,"Horn(Bodensee), SF":null,"Horn-Bad Meinberg":null,"Hornberg(Schwarzw)":null,Horneburg:null,"Horni Blatna":null,"Horni Dvoriste":null,"Horni Kamenice":null,"Horni Podluzi":null,"Horni Poustevna":null,Hornstorf:null,Horovice:null,Horrem:null,"Horsens st":null,"Horst(Holst)":null,"Horst-Sevenum":null,Hosena:null,Houten:null,"Houten Castellum":null,"Houthem-St. Gerlach":null,Howald:null,Hoyerswerda:null,"Hoyerswerda-Neustadt":null,Hoykenkamp:null,"Hradek nad Nisou":null,"Hranice na Morave":null,Hrebeny:null,Hubacker:null,"Hubertushöhe":null,"Huchem-Stammeln":null,Huckstorf:null,Hude:null,Hufschlag:null,Huglfing:null,Hugstetten:null,Hulb:null,"Hundsgrün":null,Hundstadt:null,Hungen:null,Huntlosen:null,Hurdegaryp:null,Husby:null,Husum:null,Huttenheim:null,"Huy(B)":null,Huzenbach:null,"Hviding st":null,"Hyllerslev st":null,"Häggenschwil-Winden":null,"Hähnichen":null,"Hähnlein-Alsbach":null,"Hämelerwald":null,"Hämerten":null,"Händelstraße, Karlsruhe":null,"Hässleholm Central":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örsching":null,"Hörselgau":null,"Hörstel":null,"Hörstmar(Lippe)":null,"Hösbach":null,"Hösel":null,"Höste":null,"Hövelhof":null,"Hövelriege":null,"Höxter Rathaus":null,"Höxter-Godelheim":null,"Höxter-Lüchtringen":null,"Höxter-Ottbergen":null,"Hübschstraße, Karlsruhe":null,"Hückelhoven-Baal":null,"Hüffenhardt":null,"Hüfingen Mitte":null,"Hünfeld":null,"Hüntwangen-Wil":null,"Hürth-Kalscheuren":null,"Hüttau":null,"Hütten":null,"Hüttenbusch":null,"Hüttengrund":null,"Hüttingen":null,IJlst: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,"Ilawa Glowna":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,Immensee:null,"Immensen-Arpke":null,Immenstadt:null,"Imst-Pitztal":null,Imsterberg:null,Imsweiler:null,Ingelbach:null,Ingelheim:null,Ingelmunster:null,"Ingolstadt Audi":null,"Ingolstadt Hbf":null,"Ingolstadt Nord":null,Ingwiller:null,Inheiden:null,Inningen:null,"Innsbruck Hbf":null,"Innsbruck Hötting":null,"Innsbruck Westbahnhof":null,Inowroclaw:null,"Inselstadt Malchow":null,Insheim:null,"Interlaken Ost":null,"Interlaken West":null,"Inzing/Inn":null,Iphofen:null,Ipsheim:null,"Irfersgrün":null,Irrenlohe:null,"Is-sur-Tille":null,"Iselle di Trasquera":null,"Iselle transito":null,Iserlohn:null,Iserlohnerheide:null,Isernhagen:null,Ismaning:null,Ispringen:null,"Isselhorst-Avenwedde":null,Istein:null,"Ittersbach Bahnhof":null,"Ittersbach Industrie, Karlsbad":null,"Ittersbach Rathaus":null,Ittling:null,Ittlingen:null,Itzehoe:null,Itzelberg:null,"Ivanic Grad":null,"Jabel(Meckl)":null,"Jablonne v Podjestedi":null,"Jacobsdorf(Mark)":null,Jaderberg:null,"Jagdschloß":null,Jagstzell:null,"Jahnsdorf(Erzgeb)":null,"Janderup st":null,"Jankowa Zaganska":null,"Janovice nad Uhlavou":null,Jarrenwisch:null,Jasnitz:null,Jatznick:null,"Jeber-Bergfrieden":null,Jechtingen:null,Jedlova:null,Jeeser:null,"Jegum st":null,"Jelenia Gora":null,"Jena Paradies":null,"Jena Saalbf":null,"Jena West":null,"Jena-Göschwitz":null,"Jena-Zwätzen":null,Jenbach:null,"Jenbach Zillertalbahn":null,Jennersdorf:null,Jerichow:null,Jerxheim:null,"Jesenice(Gr)":null,"Jesenice(SL)":null,"Jesewitz(Leipzig)":null,"Jessen(Elster)":null,Jestetten:null,Jettenbach:null,Jettingen:null,Jeumont:null,Jever:null,"Jeßnitz(Anh)":null,"Jiretin pod Jedlovou":null,"Jirkov zast.":null,Joachimsthal:null,"Joachimsthal Kaiserbahnhof":null,Jocketa:null,"Jockgrim Bf":null,Joeuf:null,Johanngeorgenstadt:null,"Joigny(Lar.Migennes)":null,Jossa:null,Judenburg:null,Julbach:null,"Jungingen(Hohenz)":null,Jungnau:null,Juvisy:null,"Jägersfreude":null,"Jänschwalde":null,"Jänschwalde Ost":null,"Jävenitz":null,"Jöhlingen":null,"Jöhlingen West":null,"Jößnitz":null,"Jübek":null,"Jüchen":null,"Jülich":null,"Jülich An den Aspen":null,"Jülich Forschungszentrum":null,"Jülich-Broich":null,"Jülich-Nord":null,"Jülich-Selgersdorf":null,"Jünkerath":null,"Jüterbog":null,"Jütrichau":null,"KIT-Campus Nord Bahnhof, Eggenstein-Leopoldshafen":null,"KVG-Betriebshof, Kassel":null,"Kaarst IKEA":null,"Kaarst Mitte/Holzbüttgen":null,"Kaarster Bahnhof":null,"Kaarster See":null,Kablow:null,"Kadan-Prunerov":null,"Kahl Kopp/Heide":null,"Kahl(Main)":null,"Kahla(Thür)":null,Kaiseraugst:null,Kaisersesch:null,"Kaiserslautern Galgenschanze":null,"Kaiserslautern Hbf":null,"Kaiserslautern Pfaffwerk":null,"Kaiserslautern West":null,"Kaiserslautern-Hohenecken":null,"Kaiserstuhl AG":null,Kalchreuth:null,Kaldenkirchen:null,"Kalenborn(Westerw)":null,Kalhausen:null,Kall:null,"Kalsdorf b.Graz":null,Kalsow:null,Kaltenberg:null,"Kaltenbrunnen im Montafon":null,Kalteneck:null,"Kaltenkirchen Süd":null,"Kaltenkirchen(Holst)":null,"Kalthof(Kr Iserlohn)":null,Kalwang:null,Kamen:null,"Kamen-Methler":null,"Kamenz(Sachs)":null,"Kamp-Bornhofen":null,"Kampen Zuid":null,"Kampen(NL)":null,Kandel:null,Kandern:null,Kandersteg:null,Kanzem:null,"Kapelle-Biezelinge":null,"Kapellen-Drusweiler":null,"Kapellen-Wevelinghoven":null,"Kapen Biosphärenreservat":null,Kapfenberg:null,Kappelrodeck:null,"Kappelrodeck Ost":null,Kapsweyer:null,"Karlovy Vary":null,"Karlovy Vary dolni n.":null,Karlsburg:null,Karlsdorf:null,Karlshagen:null,"Karlsruhe Albtalbahnhof":null,"Karlsruhe Bahnhofsvorplatz":null,"Karlsruhe Durlacher Tor / KIT-Campus Süd":null,"Karlsruhe Entenfang":null,"Karlsruhe Hbf":null,"Karlsruhe Hbf Südausgang":null,"Karlsruhe Marktplatz (Kaiserstraße)":null,"Karlsruhe Mühlburger Tor (Kaiserallee)":null,"Karlsruhe West":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,Katowice:null,Kattenes:null,Kattenvenne:null,Katzenfurt:null,"Katzhütte":null,Katzwang:null,Katzweiler:null,Kaub:null,Kaufbeuren:null,Kaufering:null,"Kaulsdorf(Saale)":null,Kautenbach:null,"Kavelstorf(Kr Rostock)":null,Kehl:null,Kehlen:null,Kehlhof:null,Keitum:null,"Kelenföld":null,Kelkheim:null,"Kelkheim-Hornau":null,"Kelkheim-Münster":null,"Kellmünz":null,Kelsterbach:null,"Kematen in Tirol":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,"Kerkrade Centrum":null,Kerkwitz:null,Kersbach:null,Kesswil:null,Kesteren: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-Ellerbek":null,"Kiel-Elmschenhagen":null,"Kiel-Hassee CITTI-PARK":null,"Kiel-Oppendorf":null,"Kiel-Russee":null,Kierspe:null,"Kilchberg(CH)":null,Killer:null,"Killwangen-Spreitenbach":null,Kindberg:null,"Kinding(Altmühltal)":null,Kindsbach:null,"Kirch Göns":null,"Kirch-Jesar":null,"Kirchanschöring":null,"Kirchberg in Tirol":null,"Kirchberg(Murr)":null,Kirchbichl:null,"Kirchdorf(Deister)":null,"Kirchdorf/Krems":null,Kirchehrenbach:null,Kirchen:null,Kirchenlaibach:null,"Kirchenlamitz Ost":null,Kirchentellinsfurt:null,"Kirchgasse, Kassel":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,Kirchweidach:null,Kirchweyhe:null,Kirchzarten:null,Kirkel:null,Kirn:null,"Kirnbach-Grün":null,Kirnsulzbach:null,Kirschbaumwasen:null,Kissing:null,Kittsee:null,"Kitzbühel":null,"Kitzbühel Hahnenkamm":null,Kitzingen:null,"Kißlegg":null,"Klaffenbach Hp":null,"Klagenfurt Hbf":null,Klais:null,Klandorf:null,"Klanxbüll":null,Klarenbeek:null,"Klasdorf Glashütte":null,Klatovy:null,"Klaus in Vorarlberg":null,Klecken:null,"Kledering b.Wien":null,"Klein Bünzow":null,"Klein Gerau":null,"Klein Winternheim-Ober Olm":null,Kleinberghofen:null,Kleinbettingen:null,Kleinblittersdorf:null,Kleinenbroich:null,Kleinensiel: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,"Klimmen-Ransdaal":null,Klinge:null,"Klingenberg(Main)":null,"Klingenberg-Colmnitz":null,Klingenbrunn:null,Klingenthal:null,Klingnau:null,"Klinikum Bremen-Nord/Beckedorf":null,Klitschmar:null,Klitten:null,"Kloster Bronnbach":null,"Kloster Marienthal":null,"Kloster Oesede":null,Klosterbuch:null,Klosterfelde:null,Klosterlechfeld:null,Klostermansfeld:null,"Klostermansfeld Randsiedlung":null,Klosterreichenbach:null,Kloten:null,Klotten:null,"Kläden(Stendal)":null,Knesebeck:null,"Knielingen Eggensteiner Straße, Karlsruhe":null,"Knielingen Herweghstraße, Karlsruhe":null,"Knielingen Siemens, Karlsruhe":null,"Knielinger Allee/Städt. Klinikum, Karlsruhe":null,Knittelfeld:null,"Knittlingen-Kleinvillars":null,"Knöringen-Essingen":null,"Kobern-Gondorf":null,"Koblenz Dorf":null,"Koblenz Hbf":null,"Koblenz Stadtmitte":null,"Koblenz(CH)":null,"Koblenz-Ehrenbreitstein":null,"Koblenz-Güls":null,"Koblenz-Lützel":null,"Koblenz-Moselweiß":null,Kochel:null,Kodersdorf:null,"Koebenhavn H":null,"Koebenhavns Lufthavn st":null,Kogenheim:null,Kohlscheid:null,Kohlstetten:null,Kolbermoor:null,Kolbnitz:null,"Kolding st":null,"Kolin(CZ)":null,Kolkwitz:null,"Kolkwitz Süd":null,Kollmarsreute:null,Kollnau:null,Komarom:null,Konin:null,Konstanz:null,"Konstanz Hafen":null,"Konstanz-Fürstenberg":null,"Konstanz-Petershausen":null,"Konstanz-Wollmatingen":null,Konz:null,"Konz Mitte":null,"Konzerthaus, Karlsruhe":null,"Koog aan de Zaan":null,"Korbach Hbf":null,"Korbach Süd":null,Kordel:null,Kork:null,Korntal:null,"Korntal Gymnasium":null,"Kornwestheim Pbf":null,Korschenbroich:null,"Korsoer st":null,Kortenberg:null,Kortrijk:null,Koserow:null,"Kothmaißling":null,Kottenheim:null,"Koudum-Molkwerum":null,Krabbendijke:null,Kraftsdorf:null,"Kraftwerk Finkenheerd":null,Kraghammer:null,"Krakow Glowny":null,"Kralupy nad Vltavou":null,Kranebitten:null,Kranichfeld:null,Kranj:null,Kraslice:null,"Kraslice predmesti":null,"Kraslice-Pod vlekem":null,"Krasna Lipa":null,"Krasna Lipa mesto":null,Kratzeburg:null,Krauthausen:null,Kredenbach:null,"Krefeld Hbf":null,"Krefeld-Hohenbudberg Chempark":null,"Krefeld-Linn":null,"Krefeld-Oppum":null,"Krefeld-Uerdingen":null,Kreiensen:null,"Kreimbach-Kaulbach":null,Kremmen:null,Krempe:null,Kremperheide:null,"Krems an der Donau":null,Krensitz:null,Kressbronn:null,"Kressbronn Hafen":null,"Kretscham-Rothensehma":null,"Kreuz Konz":null,"Kreuzau Bahnhof":null,"Kreuzau-Eifelstraße":null,"Kreuzberg(Ahr)":null,"Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc":null,Kreuzlingen:null,"Kreuzlingen Bernrain":null,"Kreuzlingen Hafen":null,"Kreuzstraße":null,Kreuztal:null,"Kreuztal-Littfeld":null,Kriftel:null,"Krimmeri-Meinau":null,Krimov:null,Krippen:null,"Krommenie-Assendelft":null,Kronach:null,"Kronberg Süd":null,"Kronberg(Taunus)":null,"Kronenplatz (Fritz-Erler-Str.), Karlsruhe":null,"Kronenplatz (Kaiserstraße), Karlsruhe":null,Kronshagen:null,Kronskamp:null,Kronweiler:null,Kropswolde:null,Krsko:null,Kruft:null,"Kruiningen-Yerseke":null,"Krumbach(Schwab)":null,"Krumbach(Schwab)Schule":null,Krumhermsdorf:null,Krumpa:null,"Krumpendorf/Wörthersee":null,Krupunder:null,"Krzewina Zgorzelecka":null,"Krölpa-Ranis":null,"Kröpelin":null,"Kubschütz":null,Kuchen:null,Kuchl:null,Kufstein:null,"Kullenmühle, Bad Herrenalb":null,Kulmbach:null,"Kummerow(Stralsund)":null,"Kummersdorf(Storkow)":null,Kundl:null,Kunersdorf:null,Kunowice:null,"Kupfermühle":null,Kuppenheim:null,"Kurort Altenberg(Erzgebirge)":null,"Kurort Jonsdorf":null,"Kurort Jonsdorf Hst":null,"Kurort Kipsdorf":null,"Kurort Oberwiesenthal":null,"Kurort Oybin":null,"Kurort Oybin-Niederdorf":null,"Kurort Rathen":null,"Kurt-Schumacher-Straße, Karlsruhe":null,Kusel:null,Kutenholz:null,Kutina:null,Kutno:null,Kuty:null,Kutzenhausen:null,Kyhna:null,Kyllburg:null,Kyritz:null,Kytlice: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-Holweide":null,"Köln-Longerich":null,"Köln-Mülheim":null,"Köln-Müngersdorf Technologiepark":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önigsplatz, Kassel":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örmend":null,"Köthen":null,"Köttewitz":null,"Kötzschau":null,"Kövenig":null,"Kühler Krug, Karlsruhe":null,"Kühnhausen":null,"Kühren":null,"Külte-Wetterburg":null,"Künsebeck":null,"Küntrop":null,"Küps":null,"Kürbitz":null,"Küssnacht am Rigi":null,"Küstrin-Kietz":null,LAigle: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,Laaber:null,"Laage(Meckl)":null,Laberweinting:null,Lachen:null,Ladenburg:null,"Lage Zwaluwe":null,"Lage(Lippe)":null,Lagerlechfeld:null,"Lahntal-Sarnau":null,"Lahr(Schwarzw)":null,Laineck:null,Lalendorf:null,Lam:null,Lamadelaine:null,Lambach:null,"Lambrecht(Pfalz)":null,Lambsheim:null,"Lameyplatz, Karlsruhe":null,"Lamone-Cadempino":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,"Landeck-Zams":null,Landen:null,Landgraaf:null,Landquart:null,Landry: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,Langdorp:null,"Langebrück(Sachs)":null,"Langeln(Holst)":null,Langelsheim:null,"Langen am Arlberg":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,"Langensteinbach Schießhüttenäcker, Karlsbad":null,"Langensteinbach St. Barbara, Karlsbad":null,"Langenthal(CH)":null,"Langenwang(Schwab)":null,Langenweddingen:null,Langenwolmsdorf:null,"Langenwolmsdorf Mitte":null,Langenzenn:null,Langerwehe:null,Langhagen:null,Langkampfen:null,Langlau:null,"Langsdorf(Oberhess)":null,Langwedel:null,"Langweid(Lech)":null,"Lansingerland-Zoetermeer":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(CH)":null,"Laufen(Oberbay)":null,"Laufenburg(Baden)":null,"Laufenburg(Baden)Ost":null,"Laufenburg(CH)":null,"Lauffen(Neckar)":null,Lauingen:null,"Laupheim Stadt":null,"Laupheim West":null,"Laurenburg(Lahn)":null,Lausanne:null,"Lausanne-Flon":null,"Lauscha(Thür)":null,"Lausen(CH)":null,"Lauta(Nl)":null,"Lautenbach(Baden)":null,"Lauter(Sachs)":null,Lauterach:null,"Lauterbach Mole":null,"Lauterbach(Hess)Nord":null,"Lauterbach(Rügen)":null,"Lauterbach-Steinbach":null,Lauterbourg:null,"Lauterecken-Grumbach":null,"Laußnitz":null,"Laveno Mombello":null,"Le Blanc-Mesnil":null,"Le Bourget":null,"Le Creusot Montceau Montchanin TGV":null,"Le Havre":null,"Le Mans":null,"Le Raincy Villemomble Montferm":null,Lebach:null,"Lebach-Jabach":null,Lebbeke:null,"Leer(Ostfriesl)":null,Leerdam:null,"Leese-Stolzenau":null,Leeuwarden:null,"Leeuwarden Camminghaburen":null,Legden:null,Legefeld:null,Legelshurst:null,Legnica:null,Lehmen:null,"Lehndorf(Altenburg)":null,Lehnheim:null,Lehnitz:null,Lehrte:null,Leibnitz:null,Leichlingen:null,"Leiden Centraal":null,"Leiden Lammenschans":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 Essener Straße":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 Miltitzer Allee":null,"Leipzig Mockauer Straße":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-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-Plagwitz":null,"Leipzig-Rückmarsdorf":null,"Leipzig-Sellerhausen":null,"Leipzig-Stötteritz":null,"Leipzig-Thekla":null,"Leipzig-Wahren":null,"Leipzig/Halle Flughafen":null,"Leipziger Platz, Kassel":null,"Leipziger Straße, Kassel":null,Leisnig:null,"Leithen b.Seefeld":null,Leitstade:null,"Leißling":null,"Lelystad Centrum":null,Lembeck:null,"Lemförde":null,Lemgo:null,"Lemgo-Lüttfeld":null,Lemmie:null,Lend:null,Lendringsen:null,"Lengede-Broistedt":null,"Lengefeld-Rauenstein":null,"Lengenfeld(Vogtl)":null,Lengenwang:null,"Lengerich(Westf)":null,Lenggries:null,Lenglern:null,Lengwil:null,"Lennestadt-Altenhundem":null,"Lennestadt-Grevenbrück":null,"Lennestadt-Meggen":null,"Lens(F)":null,Lensahn:null,"Lentföhrden":null,Lenzburg:null,Lenzing:null,"Leoben Hbf":null,Leogang:null,Leonberg:null,Leopoldsburg:null,"Leopoldshafen Frankfurter Straße, Eggenstein-Leopo":null,"Leopoldshafen Leopoldstr.":null,"Leopoldshafen Viermorgen, Eggenstein-Leopoldshafen":null,Leopoldstal:null,Lermoos:null,Lerouville:null,"Les Arcs Draguignan":null,"Les-Aubrais-Orleans":null,"Lesce-Bled":null,Leschede:null,"Lessingstraße, Karlsruhe":null,Letmathe:null,"Letmathe Dechenhöhle":null,Letschin:null,"Lette(Kr Coesfeld)":null,Letter:null,Leubingen:null,"Leubsdorf(Rhein)":null,"Leubsdorf(Sachs)":null,Leudelange:null,Leuk:null,"Leun/Braunfels":null,"Leuna Werke Nord":null,"Leuna Werke Süd":null,Leutenberg:null,Leuterschach:null,"Leutershausen-Wiedersbach":null,"Leutesdorf(Rhein)":null,"Leuthen(Cottbus)":null,Leutkirch:null,Leuven:null,"Leverkusen Chempark":null,"Leverkusen Mitte":null,"Leverkusen-Küppersteg":null,"Leverkusen-Rheindorf":null,"Leverkusen-Schlebusch":null,Lezignan:null,Liberec:null,Libramont: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,"Lichtenvoorde-Groenlo":null,"Liebenau(Bz Kassel)":null,"Liebenthal(Prignitz)":null,Lieblos:null,Liederbach:null,"Liederbach-Süd":null,"Lienz in Osttirol":null,Liers:null,Liestal:null,"Lietzow(Rügen)":null,Liezen:null,"Lille Europe":null,"Lille Flandres":null,"Limbach(Vogtl)":null,"Limbach(b Homburg,Saar)":null,"Limburg Süd":null,"Limburg(Lahn)":null,Limburgerhof:null,"Limmritz(Sachs)":null,Limone:null,"Linda(Elster)":null,Lindach:null,"Lindau-Aeschach":null,"Lindau-Insel":null,"Lindau-Reutin":null,"Lindenberg(Mark)":null,"Lindenberg, Kassel":null,Lindenholzhausen:null,Lindern:null,"Lindhorst(Schaumb-Lippe)":null,"Lindow(Mark)":null,Lindwedel:null,"Lingen(Ems)":null,Lingenfeld:null,"Linkenheim Friedrichstraße, Linkenheim-Hochstetten":null,"Linkenheim Rathaus":null,"Linkenheim Schulzentrum, Linkenheim-Hochstetten":null,"Linkenheim Süd, Linkenheim-Hochstetten":null,"Linköping Central":null,"Linnich Bhf":null,"Linnich-Tetz":null,Linsburg:null,Linsenhofen:null,"Linz Hbf":null,"Linz(Rhein)":null,"Linz/Donau Wegscheid":null,"Lipinki Luzyckie":null,"Lipova u Sluknova":null,Lippstadt:null,Lispenhausen:null,Lissendorf:null,Listerscheid:null,Litija:null,"Litomerice mesto":null,"Livorno Centrale":null,"Liège-Guillemins":null,Ljubljana:null,"Lobstädt":null,Locarno:null,"Lochau-Hörbranz":null,Lochem:null,Lochham:null,"Loeftgaard st":null,"Lohgarten-Roth":null,Lohhof:null,Lohmen:null,"Lohne(Oldb)":null,Lohnweiler:null,"Lohr Bahnhof":null,Lohsa:null,"Loitsch-Hohenleuben":null,Lollar:null,Longueau:null,Longwy:null,"Lons-Le-Saunier":null,Lonsee:null,"Loosdorf b.Melk":null,Loppenhausen:null,Loppersum:null,"Lorch(Rhein)":null,"Lorch(Württ)":null,Lorchhausen:null,Lorraine:null,Lorsbach:null,Lorsch:null,"Lorüns":null,Lottschesee:null,Lottstetten:null,Lourches:null,Lourdes:null,Lovosice:null,Loxstedt:null,"Loßburg-Rodt":null,"Luban Sl.":null,Lubolz:null,Luckaitztal:null,"Luckau-Uckro":null,Luckenau:null,Luckenwalde:null,Ludersheim:null,Ludesch:null,"Ludwigsau-Friedlos":null,Ludwigsburg:null,Ludwigschorgast:null,Ludwigsfelde:null,"Ludwigsfelde-Struveshof":null,"Ludwigshafen(Bodensee)":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(Rhein) Oppau":null,"Ludwigshafen-Mundenheim":null,"Ludwigshafen-Oggersheim":null,"Ludwigshafen-Rheingönheim":null,"Ludwigshöhe":null,Ludwigslust:null,Ludwigsstadt:null,Ludwigsthal:null,Lugano:null,"Luh nad Svatavou":null,Luhe:null,"Luhe-Wildenau":null,Luino:null,"Luisenthal(Saar)":null,"Lumes Halte":null,"Lund Central":null,"Lunde J st":null,Lunden:null,"Lunderskov st":null,Lunel:null,Lunestedt:null,Lunteren:null,"Lunéville":null,Lupfig:null,Lustenau:null,"Luterbach-Attisholz":null,"Lutherplatz, Kassel":null,"Lutherstadt Eisleben":null,"Lutherstadt Wittenberg Altstadt":null,"Lutherstadt Wittenberg Hbf":null,"Lutherstadt Wittenberg-Labetz":null,"Lutherstadt Wittenberg-Piesteritz":null,Lutten:null,Lutterbach:null,Lutum:null,Lutzelbourg:null,Luxembourg:null,Luzern:null,"Lyon Part Dieu":null,"Lähn":null,"Läufelfingen":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ököshaza":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ö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üdenscheid":null,"Lüdenscheid-Brügge":null,"Lüdersdorf(Meckl)":null,"Lüdinghausen":null,"Lügde":null,"Lüneburg":null,"Lünen Hbf":null,"Lünen-Preußen":null,"Lünern":null,"Lüssow(Meckl)":null,"Lütter":null,"Lützel":null,"Lützow":null,Maarheeze:null,Maarn:null,Maarssen:null,"Maasbüll(b Niebüll)":null,Maastricht:null,"Maastricht Noord":null,"Maastricht Randwyck":null,"Machern(Sachs)":null,Machnin:null,"Machnin hrad":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-Salbke":null,"Magdeburg-Sudenburg":null,Magstadt:null,Mahlow:null,Mahlwinkel:null,Maichingen:null,"Maichingen Nord":null,Maienfeld: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 Waggonfabrik":null,"Mainz-Bischofsheim":null,"Mainz-Gonsenheim":null,"Mainz-Gustavsburg":null,"Mainz-Kastel":null,"Mainz-Laubenheim":null,"Mainz-Marienborn":null,"Mainz-Mombach":null,Maisach:null,"Maishofen-Saalbach":null,"Maizieres-les-Metz":null,"Mala Velen":null,Malbork:null,Malchin:null,"Malching(Oberbay)":null,Malczyce:null,"Malk Göhren":null,Mallersdorf:null,"Malliß":null,"Mallnitz-Obervellach":null,Malmsheim:null,"Malmö Central":null,Malsch:null,"Malsch Süd":null,Malsfeld:null,"Malsfeld-Beiseförth":null,Malter:null,Mamer:null,"Mamer Lycée":null,Mammendorf:null,"Mammern URh":null,"Mammern(Bodensee)":null,Manage:null,Mandern:null,Manebach:null,Manndorf:null,"Mannenbach URh":null,"Mannenbach-Salenstein":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,"Mansfeld(Südharz)":null,Manternach:null,Mantgum: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,Marbehan:null,"Marburg Süd":null,"Marburg(Lahn)":null,"Marche-les-Dames":null,Marchegg:null,"Marchienne au Pont":null,Marchtrenk:null,"Margertshausen Bf":null,"Maria Rain":null,"Maria Veen":null,Maribor:null,"Marienberg(NL)":null,Marienborn:null,Marienhafe:null,Marienheide:null,"Markdorf(Baden)":null,Marke:null,Markelfingen:null,Markelsheim:null,Markkleeberg:null,"Markkleeberg Mitte":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,"Marktplatz, Karlsruhe":null,Marktredwitz:null,Marktschorgast:null,Markvartice:null,"Marl Mitte":null,"Marl-Hamm":null,"Marl-Sinsen":null,"Marle-sur-Serre":null,Marlishausen:null,Marloie:null,"Marne la Vallée-Chessy":null,Marnheim:null,Marquardt:null,Marsberg:null,"Marseille-Blancarde":null,"Marseille-St-Charles":null,"Marstetten-Aitrach":null,Martensdorf:null,Martenshoek:null,Martigny:null,Martigues:null,Martinlamitz:null,Martinroda:null,Martinstein:null,"Martinszell(Allgäu)":null,"Marxgrün":null,Marxzell:null,Marzling:null,Maschen:null,Maselheim:null,Massen:null,Massing:null,"Mathystraße, Karlsruhe":null,"Matrei am Brenner":null,Matzenbach:null,Matzing:null,Maubach:null,Maubeuge:null,"Mauer(b Heidelberg)":null,"Maulbronn Stadt/Kloster":null,"Maulbronn West":null,Maulburg:null,Mausheim:null,"Mautern im Liesingtal":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,"Mayrhofen im Zillertal":null,"Mayschoß":null,Mechelen: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,Medias:null,Meeder:null,Meerane:null,"Meerbusch-Osterath":null,Meerssen:null,Meeschensee:null,Mehltheuer:null,Mehrhoog:null,Meine:null,"Meinersdorf(Erzgeb)":null,Meinersen:null,Meinerzhagen:null,Meiningen:null,Meiringen:null,Meisdorf:null,Meitingen:null,Meitzendorf:null,"Meißen":null,"Meißen Altstadt":null,"Meißen Triebischtal":null,Melbach:null,Melchow:null,Meldorf:null,Melk:null,Melle:null,"Mellenbach-Glasbach":null,Mellendorf:null,Mellikon:null,"Mellingen(Thür)":null,"Mellrichstadt Bf":null,Mels:null,Melsdorf:null,Melsungen:null,"Melsungen Bartenwetzerbrücke":null,"Melsungen-Röhrenfurth":null,Melun:null,Memmingen:null,"Menden(Rheinl)":null,"Menden(Sauerland)":null,"Menden(Sauerland)Süd":null,Mendig:null,Mendrisio:null,Mengen:null,Mengeringhausen:null,"Mengersgereuth-Hämmern":null,"Mengersgereuth-Hämmern Ost":null,"Menningen-Leitishofen":null,Menton:null,"Menzingen(Baden)":null,Menznau:null,Meppel:null,Meppen:null,"Merano/Meran":null,Merching:null,Merchtem:null,Merchweiler:null,Merelbeke:null,Mering:null,"Mering-St Afra":null,"Mersch(LUX)":null,"Mersch(Westf)":null,"Merseburg Bergmannsring":null,"Merseburg Hbf":null,"Merten(Sieg)":null,Mertert:null,Mertesheim:null,"Mertingen Bahnhof":null,"Merxheim(Colmar)":null,Merzenich:null,"Merzig(Saar)":null,"Merzig(Saar) Ost":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,"Metz Nord":null,"Metz Ville":null,"Metzingen(Württ)":null,"Metzingen-Neuhausen":null,"Meuse TGV":null,"Meuselbach-Schwarzmühle":null,Meyenburg:null,"Meßdorf":null,"Meßkirch":null,"Michelau(LUX)":null,"Michelau(Oberfr)":null,"Michelau(Württ)":null,"Michelaubrück":null,"Michelbach(Unterfr)":null,Micheldorf:null,Michelstadt:null,Michendorf:null,Middelburg:null,"Miedelsbach-Steinenberg":null,Miekinia:null,Miesbach:null,Miesenbach:null,Miesenheim:null,Mieste:null,Miesterhorst:null,"Mikulasovice dolni nadrazi":null,"Milano Centrale":null,"Milano Greco Pirelli":null,"Milano Lambrate":null,"Milano Porta Garibaldi":null,"Millingen(b Rees)":null,"Millingen(b Rheinb)":null,Milmersdorf:null,Milmort:null,Miltach:null,Miltenberg:null,Miltern:null,Miltzow:null,Mimberg:null,Mimon:null,Mindelaltheim:null,Mindelheim:null,"Minden(Westf)":null,Mining:null,Miramas:null,Mirow:null,Mistorf:null,"Mittel Gründau":null,Mittelherwigsdorf:null,Mitteloelsnitz:null,Mittelschmalkalden:null,Mittelsinn:null,Mittenwald:null,"Mitterberghütten":null,"Mitterdorf-Veitsch":null,Mittergars:null,Mittweida:null,Mixdorf:null,"Mixnitz Bärenschützklamm":null,"Mlada Boleslav hl.n.":null,"Mlyny(CZ)":null,Mochenwangen:null,Mockrehna:null,Modane:null,Moers:null,Moidentin:null,Mol:null,Mols:null,"Moltkestraße/Städt. Klinikum, Karlsruhe":null,Mommenheim:null,"Monaco-Monte-Carlo":null,"Monbach-Neuhausen":null,"Monguelfo-Casies/Welsberg-Gsies":null,Monreal:null,Mons:null,Monsheim:null,Montabaur:null,"Montbéliard Ville":null,Montelimar:null,Monthey:null,"Montluçon Ville":null,Montmelian:null,"Montpellier Saint-Roch":null,Montreux:null,Monza:null,Monzingen:null,"Mook-Molenhoek":null,Moorbekhalle:null,Moosbachtal:null,"Moosbierbaum-Heiligeneich":null,Moosburg:null,Moosrain:null,"Moret-Veneux-les-Sablons":null,Morges:null,Morhange:null,Moritzburg:null,Morlesau:null,Morsum:null,"Mosbach West":null,"Mosbach(Baden)":null,"Mosbach-Neckarelz":null,Mosel:null,Moselkern:null,Mosonmagyarovar:null,Most:null,Mouchard:null,"Moulins-sur-Allier":null,Mouscron:null,Moustier:null,Moutier:null,"Moutiers-Salins-Brides-les-Bains":null,"Moyeuvre-Grande":null,Mudersbach:null,Muggensturm:null,"Muggensturm Badesee":null,"Muhr a See":null,Muizen:null,"Mulda(Sachs)":null,Muldenberg:null,"Muldenberg Floßplatz":null,"Muldenhütten":null,Muldenstein:null,"Mulhouse Ville":null,"Mulhouse-Dornach":null,"Mulsum-Essel":null,Munderkingen:null,Mundolsheim:null,Munkzwalm:null,Munsbach:null,"Munster(Metzeral)":null,"Munster(Örtze)":null,Muolen:null,"Murg(Baden)":null,"Murg(CH)":null,Murnau:null,"Murnau Ort":null,Murrhardt:null,Musau:null,Mussidan:null,Muttenz:null,"Mußbach":null,"Mâcon Ville":null,"Mâcon-Loché TGV":null,"Mägdesprung":null,"Mägerkingen":null,"Märwil":null,"Möckmühl":null,"Mögelin":null,"Mögglingen(Gmünd)":null,"Möhlin":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örsch Am Hang, Rheinstetten":null,"Mörsch Bach-West, Rheinstetten":null,"Mörsch Merkurstraße, Rheinstetten":null,"Mörsch Narzissenstraße, Rheinstetten":null,"Mörsch Rheinaustraße, Rheinstetten":null,"Mörsch Römerstraße, Rheinstetten":null,"Mörsch Rösselsbrünnle, Rheinstetten":null,"Möser":null,"Mössingen":null,"Möttingen":null,"Mötz":null,"Mücheln(Geiseltal)":null,"Mücheln(Geiseltal) Stadt":null,"Mücka":null,"Mücke(Hess)":null,"Müden(Mosel)":null,"Mügeln Bf":null,"Mühlacker":null,"Mühlacker Rößlesweg":null,"Mühlanger":null,"Mühlbach(Pirna)":null,"Mühlburg West, Karlsruhe":null,"Mühlburger Feld, Karlsruhe":null,"Mühlburger Tor (Kaiserallee), Karlsruhe":null,"Mühldorf(Oberbay)":null,"Mühldorf-Möllbrücke":null,"Mühlehorn":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 am Inn":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 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 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-Perlach":null,"München-Riem":null,"München-Solln":null,"München-Trudering":null,"München-Untermenzing":null,"München-Westkreuz":null,"Münchenbuchsee":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ünsingen(CH)":null,"Münster(Hessen)":null,"Münster(W)Zentrum Nord":null,"Münster(Westf)Hbf":null,"Münster-Albachten":null,"Münster-Amelsbüren":null,"Münster-Hiltrup":null,"Münster-Häger":null,"Münster-Mecklenbeck":null,"Münster-Roxel":null,"Münster-Sarmsheim":null,"Münster-Sprakel":null,"Münster-Wiesing":null,"Münsterlingen-Scherzingen":null,"Münstertal(Schwarzwald)":null,"Münzesheim":null,"Münzesheim Ost":null,"Mürlenbach":null,"Mürzzuschlag":null,"Müssen":null,"Naarden-Bussum":null,Nabburg:null,"Nachterstedt-Hoym":null,Nackenheim:null,Nagold:null,"Nagold Stadtmitte":null,"Nagold-Iselshausen":null,"Nagold-Steinberg":null,"Nagymaros-Visegrad":null,Naila:null,Namborn:null,Namedy:null,"Nammen-Bad":null,Namur:null,"Nancois Tronville":null,Nancy:null,Nantes:null,Narbonne: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,Naunhof:null,Neanderthal:null,Nebikon: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 Mitte":null,"Neckarsulm Nord":null,"Neckarsulm Süd":null,Neckarzimmern:null,Nedlitz:null,Neef:null,Neerpelt:null,Neetzendorf:null,Neetzka:null,"Neheim-Hüsten":null,Nehren:null,Neidenfels:null,Neidenstein:null,Neinstedt:null,Nejdek:null,"Nejdek zastavka":null,"Nejdek-Oldrichov":null,"Nejdek-Sejfy":null,"Nejdek-Sucha":null,"Nejdek-Tisova":null,Nellmersbach:null,Nemmenich:null,"Nemours St Pierre":null,"Nemsdorf-Göhrendorf":null,Nendeln:null,"Nendingen(b Tuttlingen)":null,Nennhausen:null,Nennig:null,"Nennigmühle":null,Nenzing:null,Nenzingen:null,Nersingen:null,Nesselwang:null,Nessonvaux:null,Nestedice:null,Nestemice:null,Nettersheim:null,Nettingsdorf:null,Netzeband:null,Netzkater:null,Netzschkau:null,"Neu Pudagla":null,"Neu St Jürgen":null,"Neu Wokern":null,"Neu Wulmstorf":null,"Neu-Anspach":null,"Neu-Edingen/Friedrichsfeld":null,"Neu-Isenburg":null,"Neu-Ulm":null,Neubiberg:null,Neubrandenburg:null,"Neubrücke(Nahe)":null,Neubukow:null,"Neuburg(Donau)":null,"Neuburg(Kammel)":null,"Neuburg(Rhein)":null,"Neubäu":null,"Neuchâtel":null,Neudenau:null,Neudietendorf:null,"Neudorf(Erzgeb)":null,"Neue Schenke":null,"Neuenburg(Baden)":null,"Neuenbürg(Enz)":null,"Neuenbürg(Enz) Freibad":null,"Neuenbürg(Enz) Süd":null,"Neuenbürg(Enz)-Rotenbach Eyachbrücke":null,Neuendettelsau:null,"Neuenhagen(b Berlin)":null,Neuenhaus:null,"Neuenhaus Süd":null,"Neuenkirchen(Oldb)":null,"Neuenmarkt-Wirsberg":null,Neuenrade:null,Neuenstein:null,"Neufahrn(Niederbay)":null,"Neufahrn(b Freising)":null,"Neufchateau(B)":null,"Neufchateau(F)":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(CH)":null,"Neuhausen(Cottbus)":null,"Neuhof(Kr Fulda)":null,"Neuhof(b Zossen)":null,Neukieritzsch:null,"Neukirch(Lausitz)Ost":null,"Neukirch(Lausitz)West":null,"Neukirch-Egnach":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-Kallham":null,"Neumarkt-St Veit":null,"Neumarkt/Wallersee":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,Neuratting:null,"Neureut Adolf-Ehrmann-Bad, Karlsruhe":null,"Neureut Bärenweg, Karlsruhe":null,"Neureut Welschneureuter Straße, Karlsruhe":null,"Neuruppin Rheinsberger Tor":null,"Neuruppin West":null,"Neusalza-Spremberg":null,"Neuses(b Kronach)":null,Neusorg:null,"Neuss Allerheiligen":null,"Neuss Am Kaiser":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,"Neuwiesenreben, Ettlingen":null,"Neuwirtshaus(Porscheplatz)":null,Neuzelle:null,"Neuötting":null,"Nice Ville":null,Nidda:null,Nidderau:null,"Nidderau-Eichen":null,"Nidderau-Windecken":null,"Nideggen-Brück":null,"Niebüll":null,"Niebüll Autoverladung":null,"Niebüll neg":null,Niedaltdorf:null,"Nieder Flörsheim-Dalsheim":null,"Nieder Olm":null,"Nieder Wöllstadt":null,"Nieder-Ohmen":null,Niederarnbach:null,Niederau:null,"Niederau-Tuchmühle":null,Niederbiegen:null,Niederbipp:null,Niederbobritzsch:null,Niederbrechen:null,Niederdollendorf:null,"Niederdorf(Erzgeb)":null,Niederdorfelden:null,Niederdreisbach:null,Niederdresselndorf:null,Niedererbach:null,Niederfinow:null,Niederglatt:null,"Niedergörsdorf":null,Niederhadamar:null,Niederheimbach:null,"Niederhöchstadt":null,"Niederhövels":null,Niederjosbach:null,Niederkorn: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,Niesky:null,Nieukerk:null,"Nieuw Amsterdam":null,"Nieuw Vennep":null,"Nieuwerkerk a. d. Ijssel":null,Nievenheim:null,Nievern:null,Nijkerk:null,Nijmegen:null,"Nijmegen Dukenburg":null,"Nijmegen Goffert":null,"Nijmegen Heyendaal":null,"Nijmegen Lent":null,Nijverdal:null,Niklashausen:null,"Nimburg(Baden)":null,"Nistertal-Bad Marienberg":null,Nittel:null,"Noerre Nebel st":null,"Noerreport st":null,Noertzange:null,"Nogent-le-Rotrou":null,Nohen:null,Nohfelden:null,"Nohra(Weimar)":null,"Nohra(Wipper)":null,"Noisy-le-Sec":null,Nonnenhorn: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,Nordhorn:null,"Nordhorn-Blanke":null,Nordsode:null,Nordstemmen:null,Nordwalde:null,Norf:null,Norheim:null,"Norrköping Central":null,Norsingen:null,"Northeim(Han)":null,Nortorf:null,Nossentin:null,"Notre-Dame-de-Briancon":null,"Nottuln-Appelhülsen":null,"Nova Gradiska":null,"Nova Kapela":null,"Nova Role":null,"Nova Role zastavka":null,Novara:null,"Nove Hamry":null,"Nove Zamky":null,Noveant:null,Novska:null,"Novy Bor":null,Noyon:null,Nufringen:null,Nunspeet:null,Nuth:null,"Nußberg-Schönau":null,"Ny Ellebjerg st":null,"Nyborg st":null,Nyiregyhaza:null,"Nymburk hl.n.":null,"Nässjö Central":null,"Nîmes":null,"Nöbdenitz":null,"Nördlingen":null,"Nörten-Hardenberg":null,"Nörvenich-Binsfeld":null,"Nünchritz":null,"Nürnberg Frankenstadion":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-Sandreuth":null,"Nürnberg-Schweinau":null,"Nürnberg-Stein":null,"Nürnberg-Steinbühl":null,"Nürtingen":null,"Nürtingen-Roßdorf":null,"Nürtingen-Vorstadt":null,"Nützen":null,"Nüziders":null,"Ober Ramstadt":null,"Ober Widdersheim":null,Oberachern:null,"Oberachern Bindfadenfabrik":null,Oberaichen:null,Oberalm:null,Oberammergau:null,Oberasbach:null,Oberau:null,Oberaudorf:null,"Oberbettingen-Hillesheim":null,Oberbillig:null,Oberbimbach:null,Oberboihingen:null,Oberbrechen:null,Oberburg:null,Obercarsdorf:null,Oberdachstetten:null,"Oberderdingen-Flehingen Industrie":null,Oberelchingen:null,Oberelsungen:null,Obererbach:null,Oberesslingen:null,Oberferrieden:null,Obergimpern:null,Oberglatt: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-Sterkrade":null,"Oberhofen im Inntal":null,Oberholz:null,Oberkirch:null,"Oberkirch-Köhlersiedlung":null,Oberkochen:null,Oberkorn:null,Oberkotzau:null,Oberkrozingen:null,Oberlahnstein:null,Oberlauscha:null,Oberlenningen:null,Oberlichtenau:null,Oberlindhart:null,Oberlinxweiler:null,Obermaubach:null,Obermodern:null,Obermohr:null,Obernau:null,"Obernberg-Altheim":null,"Obernburg-Elsenfeld":null,"Oberndorf(Neckar)":null,"Oberndorf(Wittgenstein)":null,"Oberneuschönberg":null,"Obernhof(Lahn)":null,Oberoderwitz:null,"Oberoderwitz Oberdorf":null,Oberottmarshausen:null,"Oberrieden(CH)":null,Oberriet: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,"Oberursel-Weiß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,Obourg:null,Obstfelderschmiede:null,Ochenbruck:null,Ochsenfurt:null,Ochsenhausen:null,Ochtmersleben:null,Ochtrup:null,Ockenheim:null,"Odenheim Bf":null,"Odenheim West":null,"Odense st":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,"Oerestad st":null,Oerlenbach:null,Oerlinghausen:null,Oermingen:null,Oertzenhof:null,Oese:null,Oesede:null,"Oesterport st":null,"Oestrich-Winkel":null,Oetrange: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,Oisterwijk:null,Okarben:null,Oker:null,"Oksboel st":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,Oldenzaal:null,"Olen(Belgien)":null,Olovi:null,Olpe:null,Olsberg:null,"Olsbrücken":null,"Olst(NL)":null,Olten:null,Ommen:null,Onville:null,Oostende:null,Oosterbeek:null,Opfikon:null,Opheusden:null,Opladen:null,"Opole Glowne":null,Oppenau:null,Oppenheim:null,"Oppenweiler(Württ)":null,Oppikon:null,Oppurg:null,Opwijk:null,"Orange(Avignon)":null,"Oranienbaum(Anh)":null,Oranienburg:null,"Oranienburg (S)":null,Orchies:null,"Orlamünde":null,"Orléans":null,Orschweier:null,Ortrand:null,Oschatz:null,"Oschersleben(Bode)":null,"Osnabrück Altstadt":null,"Osnabrück Hbf":null,"Osnabrück-Sutthausen":null,Oss:null,"Oss West":null,Ostbevern:null,"Ostendstraße, Karlsruhe":null,Osterburg:null,Osterburken:null,"Osterhofen(Nby)":null,"Osterhofen(Oberbay)":null,"Osterholz-Scharmbeck":null,Ostermundigen: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(Kr Hanau)":null,"Ostheim(b Butzbach)":null,Osthofen:null,"Ostrach Bahnhof":null,Ostrau:null,"Ostrava hl.n.":null,"Ostrava-Svinov":null,"Ostseebad Binz":null,"Ostseebad Kühlungsborn Mitte":null,"Ostseebad Kühlungsborn Ost":null,"Ostseebad Kühlungsborn West":null,Othmarsingen:null,Otrokovice: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,"Otto-Sachs-Straße, Karlsruhe":null,Ottobeuren:null,Ottobrunn:null,"Ottweiler(Saar)":null,"Otzberg Lengfeld":null,Otze:null,Otzing:null,Oudenbosch:null,"Outrup st":null,"Ovelgünne":null,Overath:null,Overveen:null,"Owen(Teck)":null,Owschlag:null,"Oy-Mittelberg":null,"Oßmannstedt":null,"Padborg st":null,"Paderborn Hbf":null,"Paderborn Kasseler Tor":null,"Paderborn Nord":null,"Paderborn-Schloss Neuhaus":null,"Paderborn-Sennelager":null,Padova:null,Paffendorf:null,"Pagny-sur-Moselle":null,Paindorf:null,Palzem:null,Pankofen:null,Pansdorf:null,Pantin:null,"Papenburg(Ems)":null,Papendorf:null,"Papierfabrik, Kaufungen":null,"Papiermühle(Stadtr)":null,Pappenheim:null,Parchim:null,"Pardubice hl.n.":null,"Paris Austerlitz":null,"Paris Est":null,"Paris Gare de Lyon":null,"Paris Montparnasse":null,"Paris Nord":null,"Paris St Lazare":null,Parkentin:null,Parndorf:null,Parsberg:null,Partenstein:null,Pasewalk:null,"Pasewalk Ost":null,"Passau Hbf":null,"Passow(Uckermark)":null,"Paternion-Feistritz":null,Patersdorf:null,Patsch:null,Pau:null,Paulinenaue:null,Paulinzella:null,Pavia:null,Pechbrunn:null,"Peenemünde":null,Pegau:null,"Peggau-Deutschfeistritz":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,Peltre:null,Penig:null,Penzberg:null,Pepinster:null,Perigueux:null,Perkam:null,Perl:null,Perleberg:null,Pernink:null,Perpignan:null,"Peschiera del Garda":null,Petange:null,Petergrube:null,Petersaurach:null,"Petersaurach Nord":null,"Petershagen Nord":null,"Petershagen(Uckerm)":null,"Petershagen-Lahde":null,Petershain:null,"Petershausen(Obb)":null,Peterskirchen:null,Petersroda:null,"Petit Croix":null,Pfaffenhain:null,Pfaffenhausen:null,"Pfaffenhofen(Ilm)":null,Pfalzel:null,Pfarrkirchen:null,Pfarrwerfen:null,Pflach:null,Pflaumloch:null,"Pforzheim Hbf":null,"Pforzheim Maihälden":null,"Pforzheim-Weißenstein":null,"Pfraundorf(Inn)":null,Pfreimd:null,"Pfronten-Ried":null,"Pfronten-Steinach":null,"Pfronten-Weißbach":null,Pfullendorf:null,Pfungstadt:null,"Pfäffikon SZ":null,"Pfäffingen":null,"Philipp-Reis-Straße, Karlsruhe":null,"Philippsburg(Baden)":null,Philippshagen:null,Philippsheim:null,"Philippstraße, Karlsruhe":null,"Pichl b.Schladming":null,Piding:null,Piensk:null,"Pill-Vomperbach":null,Pillgram:null,Pinneberg:null,"Pinnow(Uckermark)":null,"Pino transito":null,Pinzberg:null,Pirk:null,"Pirmasens Hbf":null,"Pirmasens Nord":null,Pirna:null,"Pirna-Copitz":null,"Pirna-Copitz Nord":null,"Pisa Centrale":null,Plaaz:null,Plaidt:null,Planegg:null,"Plate(Meckl)":null,Plattling:null,"Platz der Deutschen Einheit, Kassel":null,"Plau am See Bahnhof":null,"Plaue(Thür)":null,"Plauen(V) unt Bf":null,"Plauen(Vogtl) Mitte":null,"Plauen(Vogtl) ob Bf":null,"Plauen(Vogtl)-Straßberg":null,"Plauen(Vogtl)West":null,Pleinfeld:null,"Plesna(CZ)":null,Plessa:null,Plettenberg:null,Plochingen:null,"Ploiesti Vest":null,"Plzen hl.n.":null,"Plön":null,"Plüderhausen":null,"Plüschow":null,"Pockau-Lengefeld":null,Pocking:null,Pogeez:null,Poggenhagen:null,Poikam:null,Poing:null,Poitiers:null,"Pomezi nad Ohri":null,Pommelsbrunn:null,"Pommern(Mosel)":null,Pommritz:null,Ponitz:null,"Pont St Vincent":null,"Pont-Ste-Maxence":null,"Pont-a-Mousson":null,"Ponte Gardena-Laion/Waidbruck-Lajen":null,Pontresina:null,Poppenhausen:null,Pordenone:null,"Porschdorf(Pirna)":null,Porstendorf:null,"Port Bou":null,"Port Vendres Ville":null,"Porta Westfalica":null,"Porz(Rhein)":null,"Porz-Wahn":null,Posewald:null,Possenhofen:null,"Postbauer-Heng":null,"Poststraße, Karlsruhe":null,"Potsdam Charlottenhof":null,"Potsdam Griebnitzsee":null,"Potsdam Griebnitzsee (S)":null,"Potsdam Hbf":null,"Potsdam Hbf (S)":null,"Potsdam Medienstadt Babelsberg":null,"Potsdam Park Sanssouci":null,"Potsdam Pirschheide":null,"Potsdam-Babelsberg":null,"Potsdam-Rehbrücke":null,Potucky:null,"Potucky zastavka":null,"Pougues les Eaux":null,"Poznan Gl.":null,Praest:null,"Praha hl.n.":null,"Praha-Holesovice":null,"Praha-Smichov":null,"Pram-Haag":null,Pratau:null,Pratteln:null,Predeal:null,Preetz:null,Pregarten:null,"Premnitz Nord":null,"Premnitz Zentrum":null,Prenzlau:null,Prerov:null,Pressath:null,"Pressig-Rothenkirchen":null,Pretzfeld:null,"Pretzier(Altm)":null,Pretzsch:null,Priemerburg:null,"Prien a Chiemsee":null,Priestewitz:null,Prinzersdorf:null,Priort:null,Prisdorf:null,Prittitz:null,Pritzerbe:null,Pritzier:null,Pritzwalk:null,"Pritzwalk Hainholz":null,"Pritzwalk West":null,Probstzella:null,Profen:null,Profondsart:null,Prora:null,"Prora Ost":null,Prosselsheim:null,Przylep:null,"Prödel":null,"Prösen":null,"Prösen Ost":null,"Prösen West":null,"Puch bei Hallein":null,Puchheim:null,Pulheim:null,Pullach:null,"Pulling(b Freising)":null,Pulsnitz:null,"Pulsnitz Süd":null,Purmerend:null,"Purmerend Overwhere":null,"Purmerend Weidevenne":null,Pusarnitz:null,Puschendorf:null,Putbus:null,Putten:null,Puttgarden:null,Putzkau:null,"Pöchlarn":null,"Pölchow":null,"Pölling":null,"Pöllwitz":null,"Pönitz(Holst)":null,"Pönitz(Leipzig)":null,"Pörtschach am Wörther See":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-Kupferheide":null,Quendorf:null,Querfurt:null,Quevy:null,Quickborn:null,"Quickborn Süd":null,"Quickborner Straße":null,Quierschied:null,Quimper:null,Quint:null,Raaba:null,Raalte:null,Rabenau:null,"Rackith(Elbe)":null,"Rackwitz(Leipzig)":null,Radbruch:null,Raddusch:null,Radeberg: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,Radstadt:null,Rafz:null,Raguhn:null,Rahden:null,Rain:null,Raindorf: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,Rankweil:null,Ranstadt:null,"Ranzo-S. Abbondio":null,Rastatt:null,"Rastatt Beinle":null,Rastede:null,Rastow:null,"Rathaus, Kassel":null,"Rathaus/Fünffensterstraße, Kassel":null,Rathenow:null,"Rathmannsdorf(Kr Pirna)":null,"Ratingen Ost":null,"Rattenberg-Kramsach":null,Ratzeburg:null,Raubling:null,"Rauenstein(Thür)":null,"Raumland-Markhausen":null,"Raumünzach":null,Raun:null,Raunheim:null,Ravensburg:null,Ravenstein:null,"Re(I)":null,"Rebdorf-Hofmühle":null,"Rebstein-Marbach":null,Rech:null,Rechenberg:null,"Rechenberg Schule":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,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 Kurpark, Waldbronn":null,"Reichenbach im Kandertal":null,"Reichenbach(Fils)":null,"Reichenbach(Oberlausitz)":null,"Reichenbach(Vogtl) ob Bf":null,"Reichenbach(b. Ettlingen)":null,"Reichenberg(Unterfr)":null,Reichenburg:null,Reichenschwand:null,Reichersbeuern:null,"Reichertshausen(Ilm)":null,"Reichertshofen(Schwab) Bf":null,Reicholzheim:null,"Reifland-Wünschendorf":null,Reihen:null,Reil:null,Reilsheim:null,Reims: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,"Reith b.Seefeld":null,"Rejsby st":null,Rekawinkel:null,Reken:null,"Reken-Klein Reken":null,"Rekingen AG":null,Remagen:null,Remiremont:null,"Remscheid Hbf":null,"Remscheid-Güldenwerth":null,"Remscheid-Lennep":null,"Remscheid-Lüttringhausen":null,Renchen:null,Rendsburg:null,Rennes:null,Renningen:null,"Renningen Süd":null,Rennsteig:null,Rentrisch:null,Rentweinsdorf:null,Rentwertshausen:null,"Rentzschmühle":null,Retenice:null,"Rethen(Leine)":null,Retz: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,"Reutte in Tirol":null,"Reutte in Tirol Schulzentrum":null,Reuver:null,"Reußen":null,Rhade:null,"Rheda-Wiedenbrück":null,"Rheden(NL)":null,Rheinbach:null,"Rheinbach Römerkanal":null,"Rheinberg(Rheinl)":null,Rheinbrohl:null,Rheine:null,"Rheine-Mesum":null,Rheineck:null,"Rheinfelden(Baden)":null,"Rheinfelden(CH)":null,"Rheinhafen, Karlsruhe":null,"Rheinhafenstraße, Karlsruhe":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,Rhenen:null,Rhens:null,"Rheydt Hbf":null,"Rheydt-Odenkirchen":null,"Rhöndorf":null,"Ribe Noerremark st":null,"Ribe st":null,"Ribnitz-Damgarten Ost":null,"Ribnitz-Damgarten West":null,"Richen(b Eppingen)":null,Richterswil:null,Rickling:null,Ried:null,"Ried im Innkreis":null,Riederau:null,Riedlingen:null,Riedrode:null,"Riedstadt-Goddelau":null,"Riedstadt-Wolfskehlen":null,"Riegel am Kaiserstuhl Ort":null,"Riegel-Malterdingen":null,"Riegel-Malterdingen NE":null,Riehen:null,"Riehen Niederholz":null,Rieneck:null,Riesa:null,Rieschweiler:null,Rieseby:null,Rieste:null,Riestedt:null,"Rietheim(CH)":null,"Rietheim(Württ)":null,Rietschen:null,"Rietz in Tirol":null,"Riffelriß, Grainau":null,Rijssen:null,Rijswijk:null,"Rilland-Bath":null,Rimbach:null,Rimini:null,"Ringenwalde(Templin)":null,"Ringleben-Gebesee":null,Ringsheim:null,"Ringsted st":null,Rinkerode:null,Rinklingen:null,Rinnthal:null,Rinteln:null,"Rintheim Sinsheimer Straße, Karlsruhe":null,"Rio di Pusteria/Mühlbach":null,Rippberg:null,Ritschenhausen:null,Ritterhude:null,"Rivera-Bironico":null,Rivesaltes:null,"Rixheim(Mulhouse)":null,Roanne:null,Robilante:null,Roccavione:null,"Rochefort-Jemelle":null,"Rochlitz(Sachs)":null,Rockenhausen:null,Rodalben:null,Rodange: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,"Roedekro st":null,Roermond:null,Roeschwoog:null,Roggentin:null,"Roggwil-Berg":null,"Roggwil-Wynau":null,"Rohr(Thür)":null,"Rohr-Bad Hall":null,"Rohrbach(Ilm)":null,"Rohrbach(Oberbay)":null,"Rohrbach(Pfalz)":null,"Rohrbach(Saar)":null,"Rohrdorf(Oberbay)":null,Rohrenfeld:null,Roigheim:null,Roisdorf:null,"Roitzsch(Bitterf)":null,Rokycany:null,Rolandseck:null,Rollhofen:null,"Roma Termini":null,Romanshorn:null,"Romanshorn (See)":null,"Rombas-Clouange":null,Rommelshausen:null,Rommerskirchen:null,Ronet:null,"Ronneburg(Thür)":null,Ronnenberg:null,Ronshausen:null,Roodeschool:null,"Roodt/Syre":null,Roosendaal:null,Roppen:null,Rorschach:null,"Rorschach Hafen":null,"Rorschach Hafen (See)":null,"Rosbach v d Höhe":null,"Rosbach(Sieg)":null,"Rosenau(b Grafenau)":null,"Rosenbach bei Villach":null,"Rosenberg(Baden)":null,"Rosendahl-Holtwick":null,Rosenheim:null,"Rosenheim Aicherpark":null,"Rosenheim Hochschule":null,Rosenwinkel:null,"Roskilde st":null,Rosmalen:null,"Rostock Hbf":null,"Rostock Holbeinplatz":null,"Rostock Parkstraße":null,"Rostock Seehafen Nord":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,Rotava:null,"Rotenbach(Enz)":null,"Rotenburg a.d. Fulda":null,"Rotenburg(Wümme)":null,Rotenhain:null,Roth:null,"Rothenburg ob der Tauber":null,"Rothenburg(CH)":null,"Rothenbürg":null,"Rothenstein(Saale)":null,"Rothenthurm(CH)":null,Rothrist:null,Rotkreuz:null,"Rott(Inn)":null,Rottenacker:null,Rottenbach:null,"Rottenburg(Neckar)":null,Rottendorf:null,"Rotterdam Alexander":null,"Rotterdam Blaak":null,"Rotterdam Centraal":null,"Rotterdam Lombardijen":null,"Rotterdam Noord":null,"Rotterdam Zuid":null,Rottershausen:null,Rottweil:null,"Rottweil Göllsdorf":null,"Rottweil Neufra":null,"Rottweil Saline":null,"Roudnice nad Labem":null,Rouffach:null,Rovereto:null,Rovigo: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,"Rudolstadt(Thür)":null,"Rudolstadt-Schwarza":null,Ruhland:null,"Ruhlsdorf-Zerpenschleuse":null,Ruhmannsfelden:null,Ruhpolding:null,Ruhstorf:null,"Rum b.Innsbruck":null,Rumburk:null,Rumeln:null,Rummenohl:null,Runding:null,Runkel:null,Rupperswil:null,"Ruppertsgrün":null,Rupprechtstegen:null,Ruschberg:null,Ruschwedel:null,Rutesheim:null,Ruthenbeck:null,Ruurlo:null,Rybniste:null,Rzepin:null,"Rätzlingen":null,"Réding(F)":null,"Rémilly":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ümikon AG":null,"Rümlang":null,"Rümmingen":null,"Ründeroth":null,"Rüppurr Battstraße, Karlsruhe":null,"Rüppurr Ostendorfplatz, Karlsruhe":null,"Rüppurr Tulpenstraße, Karlsruhe":null,"Rüppurrer Tor, Karlsruhe":null,"Rüschlikon":null,"Rüsselbach":null,"Rüsselsheim":null,"Rüsselsheim Opelwerk":null,"Rüthi SG":null,"Saal(Donau)":null,"Saalburg(Taunus)":null,"Saalfeld(Saale)":null,Saalfelden: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,Safenwil:null,Sagard:null,Sagehorn:null,Saincaize:null,"Saint Ghislain":null,Saintes:null,Salach:null,Salem:null,"Salez-Sennwald":null,Sallach:null,Salmtal:null,Salzbergen:null,"Salzburg Aigen":null,"Salzburg Aiglhof":null,"Salzburg Gnigl":null,"Salzburg Hbf":null,"Salzburg Liefering":null,"Salzburg Mülln-Altstadt":null,"Salzburg Parsch":null,"Salzburg Sam":null,"Salzburg Süd":null,"Salzburg Taxham Europark":null,"Salzgitter-Bad":null,"Salzgitter-Immendorf":null,"Salzgitter-Lebenstedt":null,"Salzgitter-Ringelheim":null,"Salzgitter-Thiede":null,"Salzgitter-Watenstedt":null,Salzkotten:null,Salzwedel:null,Samedan:null,Samstagern:null,Samtens:null,"San Candido/Innichen":null,"Sand(Niederbay)":null,Sande:null,Sandebeck:null,Sanderbusch:null,"Sandersdorf(Bitterf)":null,"Sandershäuser Straße, Kassel":null,"Sandersleben(Anh)":null,"Sandförde":null,"Sandhagen(b Bad Dob)":null,Sandkrug:null,Sandwehle:null,"Sandweiler-Contern":null,Sangerhausen:null,"Sanitz(b Rostock)":null,"Sankt Augustin Zentrum":null,"Sanry-sur-Nied":null,Sanssouci:null,"Santpoort Noord":null,"Santpoort Zuid":null,"Sapjane(Gr)":null,Sargans:null,Sarnow:null,Sarrebourg:null,Sarreguemines:null,Sarstedt:null,"Sasbach am Kaiserstuhl":null,Sassenheim:null,Sassenroth:null,Sassnitz:null,"Sathonay Rillieux":null,Satteldorf:null,Satzvey:null,Sauerlach:null,Sauldorf:null,Saulgrub:null,Saulheim:null,"Saumur Rive Droit":null,Sauwerd:null,Saverne:null,"Schaan-Vaduz":null,Schaerbeek:null,"Schafbrücke":null,Schaffhausen:null,Schaftenau:null,Schaftlach:null,Schagen: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,Scharnitz:null,Scharstorf:null,Schechen:null,Scheemda:null,"Scheeßel":null,Scheibenberg:null,"Scheidemannplatz, Kassel":null,"Scheidt(Saar)":null,Schelklingen:null,Schemmerberg:null,Schenkenzell:null,Scheppach:null,Scherfede:null,"Scheuerfeld(Sieg)":null,Scheven:null,"Schiedam Centrum":null,Schieder:null,Schierbrok:null,Schierke:null,Schierstedt:null,Schifferstadt:null,"Schifferstadt Süd":null,Schifflange:null,Schiffweiler:null,"Schillerstraße, Karlsruhe":null,Schiltach:null,"Schiltach Mitte":null,Schimborn:null,"Schin op Geul":null,"Schindellegi-Feusisberg":null,Schinnen:null,"Schiphol (Airport)":null,"Schirgiswalde-Kirschau":null,Schirnding:null,Schkeuditz:null,"Schkeuditz West":null,Schkopau:null,"Schladen(Harz)":null,"Schladern(Sieg)":null,Schladming:null,"Schlatt(Hohenz)":null,Schlechtbach:null,Schleife:null,Schleswig:null,Schliengen:null,"Schlierbach(Schwalm-Eder-Kr.)":null,Schliersee:null,"Schlins-Beschling":null,"Schloss Gottesaue, Karlsruhe":null,"Schloss Rüppurr, Karlsruhe":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,"Schmiedeberg (Dresden)":null,"Schmiedeberg-Naundorf":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,"Schopfheim-Schlattholz":null,"Schopfloch(b Freudenstadt)":null,Schopp:null,Schorndorf:null,"Schorndorf-Hammerschlag":null,"Schortens-Heidmühle":null,Schouweiler:null,Schrezheim:null,Schrobenhausen:null,Schrozberg:null,Schruns:null,Schulen: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,"Schwarzach i Vorarl.":null,"Schwarzach-St.Veit":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,Schwaz:null,Schwechat: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-Lankow":null,"Schwerin-Margaretenhof":null,"Schwerin-Warnitz":null,"Schwerin-Wüstmark":null,"Schwerte(Ruhr)":null,"Schweta Bf":null,Schwetzingen:null,Schwieberdingen:null,Schwindebeck:null,Schwindegg:null,Schwindratzheim:null,Schwyz:null,"Schwäbisch Gmünd":null,"Schwäbisch Hall":null,"Schwäbisch Hall-Hessental":null,"Schwörstadt":null,"Schärding":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önbichl in Tirol":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önmünzach":null,"Schönow(Angerm)":null,"Schönstedt":null,"Schönwald(Oberfr)":null,"Schönwalde(Barnim)":null,"Schönwalde(Spreewald)":null,"Schönwies":null,"Schöppenstedt":null,"Schötmar":null,"Schübelbach-Buttikon":null,"Schülldorf":null,"Schüptitz":null,"Schüttorf":null,"Scuol-Tarasp":null,"Sebnitz(Sachs)":null,Sebuzin:null,Sechshelden:null,Sechtem:null,Seckach:null,Seddin:null,"Sedlitz Ost":null,"Seebach(Mühlhausen)":null,"Seebad Ahlbeck":null,"Seebad Heringsdorf":null,"Seebad Lubmin":null,Seebergen:null,Seebrugg:null,"Seefeld in Tirol":null,"Seefeld(Mark)":null,"Seefeld-Hechendorf":null,Seeg:null,Seegefeld:null,"Seehausen(Altm)":null,"Seehausen(Uckermark)":null,"Seekirchen am Wallersee":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,"Sejstrup st":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,Selzthal:null,Semmering:null,Senden:null,"Senden-Bösensell":null,Senftenberg:null,Sennfeld:null,Serams:null,Seregno:null,Serrig:null,Sersheim:null,"Sesto S. Giovanni":null,Sete:null,Seubersdorf:null,Seulberg:null,Seulbitz:null,Sevelen:null,Sevnica:null,Seybothenreuth:null,"Siebeldingen-Birkweiler":null,"Siebnen-Wangen":null,Siedenlangenbeck:null,Siedlinghausen:null,"Siegburg Bahnhof":null,"Siegburg/Bonn":null,Siegelsbach:null,Siegelsdorf:null,"Siegen Hbf":null,"Siegen-Geisweid":null,"Siegen-Weidenau":null,Siegershausen:null,Siegsdorf:null,"Sieniawa Zarska":null,"Sierck-les-Bains":null,Sierentz:null,Sierksdorf:null,"Sierre/Siders":null,Siersburg:null,Siershahn:null,"Sieversdorf(Neust/D)":null,"Siggenthal-Würenlingen":null,Sighisoara:null,Siglingen:null,Sigmaringen:null,Sigmaringendorf:null,Silbach:null,Silberhausen:null,"Silberhütte NE":null,"Silberstraße":null,Sillian:null,"Silz im Oberinntal":null,"Simbach(Inn)":null,Simeria:null,"Simmelsdorf-Hüttenbach":null,Simtshausen:null,Sinaia:null,Sindelfingen: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,"Sint-Denijs-Boekel":null,Sinzheim:null,"Sinzheim Nord":null,"Sinzig(Rhein)":null,Sinzing:null,Sion:null,Sipplingen:null,Sissach:null,Sittard:null,"Sitzendorf-Unterweißbach":null,"Skaerbaek st":null,"Skanderborg st":null,"Slagelse st":null,"Slavonski Brod":null,Sliedrecht:null,"Sliedrecht Baanhoek":null,Slubice:null,Sluknov:null,"Sluknov zast.":null,Sneek:null,"Sneek Noord":null,Soest:null,"Soest Zuid":null,"Soest(NL)":null,Soestdijk:null,Sohl:null,Sohland:null,Sokolov:null,"Solingen Grünewald":null,"Solingen Hbf":null,"Solingen Mitte":null,"Solingen Vogelpark":null,"Solingen-Schaberg":null,Sollstedt:null,Solms:null,Solnhofen:null,Solothurn:null,Solpke:null,"Soltau Nord":null,"Soltau(Han)":null,Soltendieck:null,Somain: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,Sopot:null,Sopron:null,Sorge:null,"Soroe st":null,"Sosnowiec Glowny":null,Sottrum:null,Soyen:null,Spa:null,Spaichingen:null,"Spaichingen Mitte":null,"Spangsbjerg st":null,Sparrieshoop:null,Spaubeek:null,Spay:null,Spechtritz:null,Speele:null,Speicher:null,Speikern:null,"Speyer Hbf":null,"Speyer Nord-West":null,Spicak:null,Spich:null,Spiegelau:null,Spielberg:null,"Spielfeld-Straß":null,Spiez:null,"Spinnerei, Ettlingen":null,"Spital am Pyhrn":null,"Spittal-Millstättersee":null,Sponholz:null,Spornitz:null,Spremberg:null,"Sprendlingen(Rheinhess)":null,Springe:null,"Sprötze":null,"Spöck Hochhaus, Stutensee":null,"Spöck Richard-Hecht-Schule, Stutensee":null,"St Alban":null,"St Avold":null,"St Dalmas de Tende":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 Malo":null,"St Mang":null,"St Michaelisdonn":null,"St Ottilien":null,"St Thomas":null,"St Wendel":null,"St-Amour":null,"St-Avre-la-Chambre":null,"St-Germain-des-Fosses":null,"St-Gervais-les-Bains":null,"St-Jean-de-Luz-Ciboure":null,"St-Jean-de-Maurienne Arvan":null,"St-Jory(Toulouse)":null,"St-Louis (Haut-Rhin)":null,"St-Louis-la-Chaussee":null,"St-Maurice(CH)":null,"St-Michel-Valloire":null,"St-Pierre-dAlbigny":null,"St-Pierre-des-Corps":null,"St-Priest":null,"St-Quentin(Aisne)":null,"St-Raphael-Valescure":null,"St-Sulpice-Lauriere":null,"St. Anton am Arlberg":null,"St. Anton im Montafon":null,"St. Gallen(CH)":null,"St. Gallen(CH) Haggen":null,"St. Gallen(CH) Winkeln":null,"St. Johann im Pongau":null,"St. Johann in Tirol":null,"St. Margrethen SG":null,"St. Moritz":null,"St. Valentin":null,"St.Jodok am Brenner":null,"St.Michael in Obersteiermark":null,"St.Peter-Seitenstetten":null,"St.Pölten Hbf":null,"St.Veit/Glan":null,Staad:null,Stade:null,"Stadt Rottenmann":null,"Stadt Wehlen(Sachs)":null,Stadtallendorf:null,Stadthagen:null,Stadtilm:null,Stadtoldendorf:null,Stadtprozelten:null,Stadtroda:null,Staffel:null,Staffelfelden:null,Stahringen:null,"Stainach-Irdning":null,Stambach:null,Stammbach:null,Stams:null,"Stans bei Schwaz":null,Stapelburg:null,"Stara Role":null,"Starckstraße, Karlsruhe":null,Starnberg:null,"Starnberg Nord":null,Statte:null,Stauchitz:null,Staudernheim:null,Staufen:null,"Staufen Süd":null,Stavoren:null,"Staßfurt":null,Steckborn:null,"Steckborn URh":null,"Stederdorf(Kr Uelzen)":null,Stedum:null,Steenwijk:null,Stegenwaldhaus:null,"Steilküste/Wittenbeck":null,"Stein(Traun)":null,"Stein-Säckingen":null,Steina:null,"Steinach in Tirol":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,Steinbourg:null,"Steindorf bei Straßwalchen":null,Steinebach:null,"Steinebrunn(CH)":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,Stelle:null,"Stelle DHE":null,"Stendal Hbf":null,"Stendal Vorbf":null,"Stendal-Stadtsee":null,Stenn:null,Stephansfeld:null,Sterbfritz:null,Sternfeld:null,"Sternhaus-Haferfeld":null,"Sternhaus-Ramberg":null,Sterzhausen:null,Stettbach:null,"Stetten (b. Haigerloch)":null,"Stetten am Heuchelberg":null,"Stetten(Donau)":null,"Stetten(Schwab)":null,"Stetten-Beinstein":null,"Stettfeld(Baden)":null,"Stettfeld-Weiher":null,Steyr:null,Stiege:null,"Stift Keppel-Allenbach":null,"Stockach NE":null,Stockau:null,Stockdorf:null,Stockerau:null,"Stockhausen(Lahn)":null,"Stockheim(Oberfr)":null,"Stockheim(Unterfr)":null,"Stockholm Central":null,"Stockstadt(Main)":null,"Stockstadt(Rhein)":null,"Stolberg(Harz)":null,"Stolberg(Rheinl)Gbf":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,Strasbourg:null,"Strasburg(Uckerm)":null,Strasshof:null,Straubing:null,"Straubing-Ost":null,Strausberg:null,"Strausberg (S)":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ßgräbchen-Bernsdorf":null,"Straßkirchen":null,"Straßwalchen":null,Stresa:null,"Strizivojna-Vrpolje":null,Strohkirchen:null,Strullendorf:null,"Struthütten":null,Stubben:null,Stubbenfelde:null,Stubersheim:null,Stumsdorf:null,Sturovo: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 Nürnberger Str.":null,"Stuttgart Schwabstr.":null,"Stuttgart Stadtmitte":null,"Stuttgart Universität":null,"Stuttgart-Bad Cannstatt":null,"Stuttgart-Feuerbach":null,"Stuttgart-Münster":null,"Stuttgart-Obertürkheim":null,"Stuttgart-Rohr":null,"Stuttgart-Sommerrain":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,"Sulz-Röthis":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,Sulzbachtal:null,Sulzberg:null,"Sulzfeld(Baden)":null,Summerau:null,Sursee:null,"Survilliers Fosses":null,Susteren:null,Svatava:null,"Svatava zastavka":null,Svor:null,Swalmen:null,Swiebodzin:null,"Swinoujscie Centrum":null,"Swisttal-Odendorf":null,Syke:null,Syrau:null,Sythen:null,"Szczecin Glowny":null,"Szczecin Gumience":null,"Szentgotthárd":null,Szob:null,"Szob(Gr)":null,Szolnok:null,"Sättelstädt":null,"Sélestat":null,"Södertälje Syd station":null,"Söllingen Kapellenstraße":null,"Söllingen Reetzstr.":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,"TGV Haute Picardie":null,Taben:null,Tabor:null,Tacherting:null,Tamines: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,"Tarascon sur Rhone":null,Tarp:null,"Tarvisio Boscoverde":null,Tata:null,Tatabanya:null,Tating:null,"Taubenheim(Spree)":null,Tauberbischofsheim:null,Tauberfeld:null,"Taucha(Leipzig)":null,Taufkirchen:null,"Taufkirchen an der Pram":null,Tautenhain:null,"Taverne-Torricella":null,"Taxenbach-Rauris":null,Tczew:null,Tecknau:null,Tegelen:null,Tegernsee:null,Teicha:null,Teichland:null,Teichwolframsdorf:null,Teisendorf:null,Teisnach:null,"Teisnach Rohde+Schwarz":null,"Telfs-Pfaffenhofen":null,Telgte:null,Teltow:null,"Teltow Stadt":null,Temmels:null,Templeuve:null,Templin:null,"Templin Stadt":null,"Templin-Ahrensdorf":null,"Tende(F)":null,"Teningen-Mundingen":null,Tenneck:null,"Teplice v Cechach":null,Terborg:null,"Terfens-Weer":null,Tergnier:null,Teschenhagen:null,Teschow:null,Tessin:null,"Tessin West":null,Testelt:null,Teterow:null,"Teting (Moselle)":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(Erzgeb)":null,"Thalheim(b Oschatz)":null,Thalwil: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,Thionville:null,"Thoßfell":null,Thun:null,Thusis:null,Thyrow:null,"Thüngersheim":null,"Thür":null,Tiebensee:null,Tiefenau:null,"Tiefenbach(b Passau)":null,"Tiefenbachmühle":null,Tiefenort:null,"Tieffenbach-Struth":null,Tiel:null,"Tiel Passewaaij":null,Tienen:null,"Tiengen(Hochrhein)":null,Tilburg:null,"Tilburg Reeshof":null,"Tilburg Universiteit":null,"Timmendorfer Strand":null,"Tinglev st":null,Tisis:null,Titisee:null,"Tittmoning-Wiesmühl":null,"Tivoli, Karlsruhe":null,"Tjaereborg st":null,"Tobel-Affeltrangen":null,"Toender Nord st":null,"Toender st":null,Tongeren:null,Torgau:null,Torgelow:null,"Torino Porta Susa":null,Tornesch:null,Tostedt:null,Toul:null,Toulon:null,"Toulouse-Matabiau":null,Tourcoing:null,Tournai:null,Tournan:null,"Traben-Trarbach":null,Trabitz:null,"Trais-Horloff":null,Trasadingen:null,Trassenheide:null,Trassenmoor:null,Traun:null,"Traun OÖ":null,Traundorf:null,Traunreut:null,Traunstein:null,"Traunstein Klinikum":null,Trbovlje:null,Trebbin:null,"Treben-Lehma":null,Trebgast:null,"Trebitz(Elbe)":null,"Trebnitz(Mark)":null,Trebusice:null,Trechtingshausen:null,"Treibach-Althofen":null,"Treis-Karden":null,Trento:null,Treuchtlingen:null,Treuen:null,Treuenbrietzen:null,"Treuenbrietzen Süd":null,"Treviso Centrale":null,Treysa:null,Triangel:null,Triberg:null,Trieben: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,Troisvierges:null,Trompet:null,Trooz:null,"Trossingen Bahnhof":null,"Trossingen Stadt":null,Trostberg:null,Tschagguns:null,"Tullastraße/Verkehrsbetriebe, Karlsruhe":null,Tulling:null,"Tulln a.d.Donau":null,Tullnerfeld:null,Tuplice:null,"Tuplice Debinka":null,Turgi:null,Tuttlingen:null,"Tuttlingen Gänsäcker":null,"Tuttlingen Nord":null,"Tuttlingen Schulen":null,"Tuttlingen Zentrum":null,Tutzing:null,Twello:null,Twiste:null,Twistringen:null,Tychy: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,Uckange:null,Uder:null,Udine: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,Uhyst:null,Uitgeest:null,Uithuizen:null,Uithuizermeeden:null,Ulberndorf:null,Ulbersdorf:null,"Ulm Hbf":null,"Ulm Ost":null,"Ulm-Donautal":null,"Ulm-Söflingen":null,"Ulmerfeld-Hausmening":null,"Ulrichsbrücke-Füssen":null,"Ulzburg Süd":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,"Unterberg-Stefansbrücke":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,"Unterneustädter Kirchplatz, Kassel":null,Unterreichenbach:null,"Unterschleißheim":null,"Untersteinach(Bayr)":null,"Untersteinach(b Stadtsteinach)":null,Unterterzen:null,Unterwellenborn:null,Unterwiesenthal:null,"Unteröwisheim Bf":null,"Unteröwisheim M.-Luther-Str.":null,Unzmarkt: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,Usquert:null,Usseln:null,"Usti nad Labem hl.n.":null,"Usti nad Labem zapad":null,"Usti nad Labem-Strekov":null,"Utrecht Centraal":null,"Utrecht Leidsche Rijn":null,"Utrecht Lunetten":null,"Utrecht Maliebaan":null,"Utrecht Overvecht":null,"Utrecht Terwijde":null,"Utrecht Vaartsche Rijn":null,"Utrecht Zuilen":null,Utting:null,Uttwil:null,Utzedel:null,Vac:null,Vach:null,Vacha:null,Vachdorf:null,Vachendorf:null,Vahldorf:null,"Vaihingen(Enz)":null,"Vaihingen(Enz)Nord":null,"Vaires Torcy":null,"Val-de-Reuil":null,"Valdaora-Anterselva/Olang-Antholz":null,Valdek:null,"Valence TGV":null,"Valence Ville":null,Valenciennes:null,"Valkenburg(NL)":null,Vallendar:null,"Vamdrup st":null,Vandans:null,"Varangeville-St-Nicolas":null,"Varde Kaserne st":null,"Varde Vest st":null,"Varde st":null,"Varel(Oldb)":null,Varnsdorf:null,"Varnsdorf Pivovar Kocour":null,"Varnsdorf stare nadr":null,Varsseveld:null,Vastorf:null,Vaterstetten:null,Vatterode:null,"Vatteröder Teich":null,Vechelde:null,Vechta:null,"Vechta-Stoppelmarkt":null,Veendam:null,"Veenendaal Centrum":null,"Veenendaal West":null,"Veenendaal-De Klomp":null,Vehlefanz:null,Veilsdorf:null,"Veitshöchheim":null,"Vejle st":null,"Velbert Rosenhügel":null,"Velbert-Langenberg":null,"Velbert-Neviges":null,"Velbert-Nierenhof":null,"Velden am Wörther See":null,"Velden(b Hersbruck)":null,Velgast:null,"Velke Zernoseky":null,"Velky Senov":null,"Velky Senov zast.":null,"Vellmar-Niedervellmar":null,"Vellmar-Obervellmar":null,"Vellmar-Osterberg/EKZ":null,Velp:null,"Velten(Mark)":null,Vendenheim:null,"Venezia Mestre":null,"Venezia Santa Lucia":null,Venissieux:null,Venlo:null,Venray:null,Ventimiglia:null,Ventschow:null,Vercelli:null,"Verden(Aller)":null,Veringendorf:null,Veringenstadt:null,Vernante:null,Vernawahlshausen:null,"Verneuil lEtang":null,"Verneuil sur Avre":null,"Vernon(Eure)":null,"Verona Porta Nuova":null,"Verviers Central":null,"Verviers-Palais":null,"Vesele pod Rabstejnem":null,Vetschau:null,"Vettweiß":null,"Vettweiß-Jakobwüllesheim":null,Vicenza:null,Viechtach:null,Vienenburg:null,Vienne:null,"Vierenstraße":null,"Vierkirchen-Esterhofen":null,Vierlingsbeek:null,Viernau:null,Viersen:null,Vieselbach:null,Vievola:null,"Vilemov u Sluknova":null,"Villabassa-Braies/Niederdorf-Prags":null,"Villach Hbf":null,"Villach Warmbad":null,"Villach Westbf":null,"Villars les Dombes":null,"Villedieu les Poeles":null,"Villers Cotterets":null,"Villiers-le-Bel-Gonesse":null,"Villingen(Schwarzw)":null,"Villingen-Schwenningen Eisstadion":null,"Villingen-Schwenningen Hammerstatt":null,Villmar:null,"Vils Stadt":null,"Vils in Tirol":null,Vilsbiburg:null,Vilseck:null,"Vilshofen(Niederbay)":null,Vilvoorde:null,Vinkovci:null,Vinzelberg:null,"Vipiteno-Val di Vizze/Sterzing-Pfitsch":null,"Visby st":null,Vise:null,Visp:null,"Visselhövede":null,"Vitry le François Gare":null,Vittel:null,Vleuten:null,Vlissingen:null,"Vlissingen Souburg":null,Vlotho:null,"Voerde(Niederrhein)":null,Voerendaal:null,"Vogelsang(Gransee)":null,Vogelweh:null,Vohburg:null,Vohren:null,"Voigtsgrün":null,Voigtstedt:null,"Vojens st":null,Vojtanov:null,Voldagsen:null,"Volders-Baumkirchen":null,"Volkach-Astheim":null,Volkmarsen:null,Volkringhausen:null,"Volkswohnung/Staatstheater, Karlsruhe":null,Volpriehausen:null,Voorburg:null,Voorhout:null,Voorschoten:null,"Voorst-Empe":null,Vorden:null,Vorhop:null,Vormwald:null,"Vormwald Dorf":null,"Vorra(Pegnitz)":null,"Voßloch":null,Vriezenveen:null,"Vroegum st":null,Vroomshoop:null,Vught:null,"Vysoka Pec":null,"Vöcklabruck":null,"Vöcklamarkt":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,"Völs":null,"Wabern(Bz Kassel)":null,"Wachenheim(Pfalz)":null,Wackershofen:null,Waddinxveen:null,"Waddinxveen Noord":null,"Waddinxveen Triangel":null,Waffenbrunn:null,Wagersrott: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,"Wald am Schoberpass":null,Walddrehna:null,"Waldenburg(Sachs)":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-Kraiburg":null,"Waldmünchen":null,Waldshut:null,Walenstadt:null,Walferdange:null,"Walhausen(Saar)":null,"Walheim(Württ)":null,Walkenried:null,"Wallau(Lahn)":null,"Walldorf(Hess)":null,"Walldorf(Werra)":null,"Walldürn":null,Wallenrod:null,Wallersdorf:null,Wallertheim:null,Walleshausen:null,"Wallhausen(Helme)":null,"Wallhausen(Württ)":null,Wallisellen:null,"Wallwitz(Saalkr)":null,Walpertskirchen:null,Walporzheim:null,Walschleben:null,Walsleben:null,Walsrode:null,Waltershausen:null,"Waltershausen Schnepfenthal":null,"Walygator Parc":null,Wandersleben:null,Wandlitz:null,Wandlitzsee:null,"Wangen(Allgäu)":null,"Wangen(Unstrut)":null,Wangerooge: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,Warffum:null,Warmbad:null,"Warnemünde":null,"Warnemünde Werft":null,Warngau:null,"Warnitz(Uckermark)":null,"Warszawa Centralna":null,"Warszawa Wschodnia":null,"Warszawa Zachodnia":null,"Wartberg im Mürztal":null,"Wartberg/Krems":null,Warthausen:null,Wasbek:null,"Wasen, Ettlingen":null,Wasenweiler:null,Wasseralfingen:null,Wasserbillig: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,Watenstedt:null,Waterloo:null,"Watermael/Watermaal":null,Wattenscheid:null,"Wattenscheid-Höntrop":null,Watzelsteg:null,"Watzenborn-Steinberg":null,"Waßmannsdorf":null,Webau:null,Wecker:null,Weckesheim:null,"Weddel(Braunschw)":null,"Wedel(Holst)":null,Weener:null,Weert:null,Weesenstein:null,Weesp:null,Weetzen:null,Weeze:null,Wefensleben:null,Wega:null,Wegberg:null,Wegeleben:null,Wegenstedt:null,Wegliniec:null,Wehdel:null,Wehl:null,"Wehr(Mosel)":null,"Wehr-Brennet":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,"Weinbrennerplatz, Karlsruhe":null,"Weinböhla Hp":null,Weinfelden:null,"Weingarten Berg":null,"Weingarten(Baden)":null,"Weinheim(Bergstr)Hbf":null,"Weinheim-Lützelsachsen":null,Weinsberg:null,"Weinsberg West":null,"Weinsberg/Ellhofen Gewerbegebiet":null,"Weinweg, Karlsruhe":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,Welkenraedt:null,Welkers:null,"Wellen(Magdeburg)":null,"Wellen(Mosel)":null,Wellendorf:null,Wellmitz:null,"Wels Hbf":null,"Welschen Ennest":null,"Welschingen-Neuhausen":null,Welver:null,"Wemmetsweiler Rathaus":null,"Wendisch Evern":null,"Wendisch-Rietz":null,"Wendling b.Haag":null,"Wendlingen(Neckar)":null,Wennedach:null,"Wennigsen(Deister)":null,Wensickendorf:null,Werbig:null,Werdau:null,"Werdau Nord":null,"Werder(Havel)":null,"Werderstraße, Karlsruhe":null,Werdohl:null,Werdorf:null,Werfen:null,Werl:null,"Wernau(Neckar)":null,Wernberg:null,"Werne a d Lippe":null,Werneuchen:null,Wernfeld:null,"Wernigerode Elmowerk":null,"Wernigerode Hbf":null,"Wernigerode Hochschule Harz":null,"Wernigerode Westerntor":null,"Wernigerode-Hasserode":null,Wernshausen:null,Wernstein:null,"Wertach-Haslach":null,Wertheim:null,"Wertheim-Bestenheid":null,Werther:null,Wesel:null,"Wesel Feldmark":null,"Wesel-Blumenkamp":null,Wesenberg:null,"Wespelaar-Tildonk":null,Wesselburen:null,Wesseln:null,Westbarthausen:null,Westbevern:null,Westendorf:null,"Westendorf in Tirol":null,Westerburg:null,Westerham:null,Westerhausen:null,"Westerland (Sylt) Autoverladung":null,"Westerland(Sylt)":null,"Westerstede-Ocholt":null,Westerstetten:null,Westervoort: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,Wettingen:null,Wetzlar:null,Wezep:null,"Weßling(Oberbay)":null,"Wickede(Ruhr)":null,Wicklesgreuth:null,Wickrath:null,Wiebelskirchen:null,Wiemersdorf:null,"Wien Floridsdorf":null,"Wien Franz-Josefs-Bahnhof":null,"Wien Hbf":null,"Wien Hbf (Autoreisezuganlage)":null,"Wien Hernals":null,"Wien Hütteldorf":null,"Wien Jedlersdorf":null,"Wien Kaiserebersdorf":null,"Wien Meidling":null,"Wien Mitte":null,"Wien Penzing":null,"Wien Praterstern":null,"Wien Simmering":null,"Wien Stadlau":null,"Wien Süßenbrunn":null,"Wien Westbahnhof":null,"Wiener Neustadt Hbf":null,"Wiener Straße, Kassel":null,Wierden: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,Wijchen:null,Wijhe:null,"Wil SG":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,Wildon:null,"Wilferdingen-Singen":null,Wilgartswiesen:null,Wilhelmsdorf:null,Wilhelmshaven:null,Wilhelmshorst:null,"Wilhelmshütte(Lahn)":null,"Wilhelmsstraße/Stadtmuseum, Kassel":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,"Wilnsdorf-Rudersdorf":null,Wilsenroth:null,Wilster:null,Wilthen:null,"Wiltingen(Saar)":null,Wilwerwiltz:null,Wilwisheim:null,Wincheringen:null,"Winden(Pfalz)":null,Windischeschenbach:null,Windischgarsten:null,Windsbach:null,"Wingen-sur-Moder":null,Wingerode:null,Wingst:null,Winkelhaid:null,Winnenden:null,"Winningen(Mosel)":null,Winninghausen:null,Winnweiler:null,Winschoten:null,"Winsen(Luhe)":null,Winsum:null,"Winterbach(b Schorndorf)":null,"Winterberg(Westf)":null,Winterhausen:null,Wintermoor:null,Winterswijk:null,"Winterswijk West":null,Winterthur:null,Wipperdorf:null,Wippra:null,Wirges:null,Wirtheim:null,Wismar:null,Wissembourg:null,"Wissen(Sieg)":null,Wissingen:null,"Wittbräucke":null,"Witten Hbf":null,"Witten-Annen Nord":null,Wittenbach: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,Wjasma:null,Woerden:null,Woffleben:null,"Wohlen AG":null,Wohltorf:null,Woippy:null,Wolfach:null,"Wolfartsweierer Straße, Karlsruhe":null,Wolfegg:null,"Wolfen(Bitterfeld)":null,"Wolfenbüttel":null,Wolferode:null,"Wolfgang(Kr Hanau)":null,Wolfhagen:null,Wolfheze:null,Wolfratshausen:null,"Wolfsburg Hbf":null,"Wolfsgefärth":null,"Wolfsmünster":null,Wolfstee:null,Wolfstein:null,Wolfurt:null,Wolgast:null,"Wolgast Hafen":null,"Wolgaster Fähre":null,Wolkenstein:null,"Wolkersdorf im Weinviertel":null,Wolkramshausen:null,"Wollbach(Baden)":null,Wolmirstedt:null,"Wolterdingen(Han)":null,"Woltersdorf/Nuthe-Urstromtal":null,Woltwiesche:null,Wolvega:null,Workum:null,Wormerveer:null,"Worms Hbf":null,"Worms-Pfeddersheim":null,Worpswede:null,Wremen:null,Wriezen:null,Wrist:null,"Wroclaw Glowny":null,"Wroclaw Lesnica":null,"Wroclaw Nowy Dwor":null,"Wulfen(Anh)":null,"Wulfen(Westf)":null,Wulften:null,Wullenstetten:null,"Wunsiedel-Holenbrunn":null,Wunstorf:null,"Wuppertal Hbf":null,"Wuppertal-Barmen":null,"Wuppertal-Hahnenfurth/Düssel":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)":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ädenswil":null,"Wölfershausen":null,"Wölfersheim-Södel":null,"Wörgl Hbf":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,"Ybbs a.d. Donau":null,"Yorckstraße, Karlsruhe":null,"Yverdon-les-Bains":null,"Yves-Gomezee":null,"ZOB, Duderstadt":null,Zaandam:null,"Zaandam Kogerveld":null,"Zaandijk Zaanse Schans":null,Zabeltitz:null,Zachun:null,Zagan:null,Zagorje:null,"Zagreb Glavni kolodvor":null,Zahna:null,Zainhammer:null,Zaisenhausen:null,Zaltbommel:null,"Zandvoort aan Zee":null,Zapfendorf:null,Zarrendorf:null,Zary:null,Zasieki:null,Zawiercie:null,Zbaszynek:null,Zedelgem:null,"Zeebrugge-Dorp":null,Zeesen:null,"Zehdenick(Mark)":null,"Zehdenick-Neuhof":null,Zeil:null,Zeithain:null,Zeitz:null,"Zelezna Ruda centrum":null,"Zelezna Ruda mesto":null,"Zell am See":null,"Zell am Ziller":null,"Zell(Harmersbach)":null,"Zell(Wiesental)":null,"Zell-Romrod":null,"Zella-Mehlis":null,"Zella-Mehlis West":null,Zellendorf:null,Zellerthal:null,Zeltweg:null,Zempin:null,Zennern:null,"Zepernick(Bernau)":null,Zeppelinheim:null,"Zerbst/Anhalt":null,Zerkall:null,Zermatt:null,Zernsdorf:null,Zerrenthin:null,"Zetten-Andelst":null,"Zeulenroda unt Bf":null,"Zeutern Bf":null,"Zeutern Ost":null,"Zeutern Sportplatz":null,Zeuthen:null,Zeutsch:null,Zevenaar:null,Zevenbergen:null,Zgorzelec:null,"Zgorzelec Miasto":null,Zichem:null,"Zidani Most":null,"Ziegelbrücke":null,Zielitz:null,"Zielitz Ort":null,"Zielona Gora Gl.":null,Zierenberg:null,"Zierenberg-Rosental":null,Ziesar:null,Zieverich:null,Zillendorf:null,Ziltendorf:null,"Zimmern(Main-Tauber)":null,"Zimmern(b Seckach)":null,Zimmersrode:null,Zinnowitz:null,Zirl:null,Zirndorf:null,"Zirndorf Kneippallee":null,"Zirovice-Seniky":null,"Zirtow-Leussow":null,Zittau:null,"Zittau Hp":null,"Zittau Süd":null,"Zittau Vorstadt":null,Zizers:null,Zoblitz:null,Zoetermeer:null,"Zoetermeer Oost":null,Zofingen:null,Zolder:null,"Zollhaus(Villingen-Schwenningen)":null,"Zollhaus-Petersthal":null,Zopten:null,Zorneding:null,Zossen:null,Zotzenbach:null,Zschaitz:null,Zscherben:null,Zschopau:null,"Zschopau Ost":null,Zschortau:null,"Zug(CH)":null,Zuidbroek:null,Zuidhorn:null,Zusenhofen:null,Zutphen:null,Zuzenhausen:null,"Zweibrücken Hbf":null,Zweidlen: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,"Zwijndrecht(NL)":null,"Zwingenberg(Baden)":null,"Zwingenberg(Bergstr)":null,Zwolle:null,"Zwolle Stadshagen":null,Zwota:null,"Zwota-Zechenbach":null,Zwotental:null,"Zwönitz":null,"Zöberitz":null,"Zörnigall":null,"Zühlsdorf":null,"Zülpich":null,"Zürich Altstetten":null,"Zürich Enge":null,"Zürich Flughafen":null,"Zürich HB":null,"Zürich Hardbrücke":null,"Zürich Oerlikon":null,"Zürich Stadelhofen":null,"Zürich Wiedikon":null,"Zürich Wollishofen":null,"Züssow":null,"Züttlingen":null,"s-Hertogenbosch":null,"s-Hertogenbosch Oost":null,"t Harde":null,"Äpfingen":null,"Öhringen Hbf":null,"Öhringen West":null,"Öhringen-Cappel":null,"Ölbronn-Dürrn":null,"Ötigheim":null,"Ötisheim":null,"Ötztal":null,"Übach-Palenberg":null,"Überlingen":null,"Überlingen Therme":null,"Überlingen-Nußdorf":null,"Übersee":null,"Ückeritz":null,"Üdingen":null,"Ürzig(DB)":null}})});
diff --git a/public/static/js/geolocation.js b/public/static/js/geolocation.js
index 50801a5..03857a1 100644
--- a/public/static/js/geolocation.js
+++ b/public/static/js/geolocation.js
@@ -1,55 +1,71 @@
/*
- * Copyright (C) 2020 Daniel Friesel
+ * 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) {
@@ -61,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 1cc7b33..54633f8 100644
--- a/public/static/js/geolocation.min.js
+++ b/public/static/js/geolocation.min.js
@@ -1 +1 @@
-$(document).ready(function(){function r(){return $("div.geolocation div.progress")}function e(e){$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},i)}function t(e){e.code==e.PERMISSION_DENIED?o("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?o("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?o("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):o("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}function n(){a.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>')),navigator.geolocation.getCurrentPosition(e,t)}var o=function(e,t,n){r().remove();var o=$(document.createElement("div"));o.attr("class","error"),o.text(t);t=$(document.createElement("strong"));t.text(e),o.prepend(t),$("div.geolocation").append(o)},i=function(e){e.error?o("Backend-Fehler:",e.error,null):0==e.candidates.length?o("Keine Bahnhöfe in 70km Umkreis gefunden","",null):(resultTable=$("<table><tbody></tbody></table>"),resultBody=resultTable.children(),$.each(e.candidates,function(e,t){var n=t.ds100,o=t.name,t=(t.distance.toFixed(1),$(document.createElement("a")));t.attr("href",n),t.text(o),resultBody.append('<tr><td><a href="/s/'+n+'">'+o+"</a></td></tr>")}),r().replaceWith(resultTable))},a=$("div.geolocation > button");a.length&&(navigator.geolocation?navigator.permissions?navigator.permissions.query({name:"geolocation"}).then(function(e){"prompt"===e.state?a.on("click",n):n()}):a.on("click",n):o("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
+$(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 7558e69..48e878f 100644
--- a/public/static/js/travelynx-actions.js
+++ b/public/static/js/travelynx-actions.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 Daniel Friesel
+ * Copyright (C) 2020 Birte Kristina Friesel
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@@ -8,8 +8,22 @@ 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(';');
@@ -36,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');
}
@@ -65,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) {
@@ -96,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);
@@ -109,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;
@@ -137,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;
}
}
@@ -152,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);
});
@@ -163,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() {
@@ -188,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);
@@ -209,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);
}
@@ -255,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 5e24b7d..8b99a82 100644
--- a/public/static/js/travelynx-actions.min.js
+++ b/public/static/js/travelynx-actions.min.js
@@ -1 +1 @@
-var j_departure=0,j_duration=0,j_arrival=0,j_dest="",j_stops=[];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);t=$(this).data("dest");t&&(j_dest=t);var e=$(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?$(".countdown").text("Ankunft in "+Math.round((j_arrival-t)/60)+" Minuten"):$(".countdown").text("Ziel erreicht"))}function hhmm(t){var e=new Date(1e3*t),t=e.getHours(),e=e.getMinutes();return(t<10?"0"+t:t)+":"+(e<10?"0"+e:e)}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=$('<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;$(".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(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],r=j_stops[stop][3],i=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!=i&&0<i-t){$(".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),e={action:"checkin",station:t.data("station"),train:t.data("train"),dest:t.data("dest")};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.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"),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 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");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)),$("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 38d95e1..ed2760a 100644
--- a/public/static/manifest.json
+++ b/public/static/manifest.json
@@ -3,27 +3,27 @@
"short_name": "Travelynx",
"scope": "/",
"icons": [{
- "src": "/static/v38/icons/icon-128x128.png",
+ "src": "/static/v72/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
- "src": "/static/v38/icons/icon-144x144.png",
+ "src": "/static/v72/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
- "src": "/static/v38/icons/icon-152x152.png",
+ "src": "/static/v72/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
- "src": "/static/v38/icons/icon-192x192.png",
+ "src": "/static/v72/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
- "src": "/static/v38/icons/icon-256x256.png",
+ "src": "/static/v72/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}, {
- "src": "/static/v38/icons/icon-512x512.png",
+ "src": "/static/v72/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
diff --git a/public/static/v37 b/public/static/v71
index 945c9b4..945c9b4 120000
--- a/public/static/v37
+++ b/public/static/v71
diff --git a/public/static/v38 b/public/static/v72
index 945c9b4..945c9b4 120000
--- a/public/static/v38
+++ b/public/static/v72
diff --git a/sass/components/_carousel.scss b/sass/components/_carousel.scss
index cc36d4b..0ec6730 100644
--- a/sass/components/_carousel.scss
+++ b/sass/components/_carousel.scss
@@ -66,7 +66,7 @@
.indicator-item {
&.active {
- background-color: #fff;
+ background-color: $off-black;
}
display: inline-block;
@@ -75,7 +75,7 @@
height: 8px;
width: 8px;
margin: 24px 4px;
- background-color: rgba(255,255,255,.5);
+ background-color: $inactive-color;
transition: background-color .3s;
border-radius: 50%;
diff --git a/sass/components/_variables.scss b/sass/components/_variables.scss
index 4c59c12..fa03d61 100644
--- a/sass/components/_variables.scss
+++ b/sass/components/_variables.scss
@@ -165,6 +165,7 @@ $dropdown-item-height: 50px !default;
// 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;
diff --git a/sass/components/forms/_forms.scss b/sass/components/forms/_forms.scss
index 4c19f4c..a387260 100644
--- a/sass/components/forms/_forms.scss
+++ b/sass/components/forms/_forms.scss
@@ -10,7 +10,7 @@ button:focus {
label {
font-size: $label-font-size;
- color: $input-border-color;
+ color: $input-label-color;
}
@import 'input-fields';
diff --git a/sass/components/forms/_input-fields.scss b/sass/components/forms/_input-fields.scss
index f18c2f8..09fa47c 100644
--- a/sass/components/forms/_input-fields.scss
+++ b/sass/components/forms/_input-fields.scss
@@ -176,7 +176,7 @@ textarea.materialize-textarea {
margin-bottom: 1rem;
& > label {
- color: $input-border-color;
+ color: $input-label-color;
position: absolute;
top: 0;
left: 0;
diff --git a/sass/src/common/index.scss b/sass/src/common/index.scss
index 9ceca2c..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 {
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 7dcd006..6665269 100644
--- a/sass/src/dark/_variables.scss
+++ b/sass/src/dark/_variables.scss
@@ -11,7 +11,7 @@ $secondary-color: color('cyan', 'darken-2');
$link-color: color('light-blue', 'darken-1');
$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');
@@ -21,3 +21,6 @@ $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 f46aefb..28c615b 100644
--- a/sass/src/dark/index.scss
+++ b/sass/src/dark/index.scss
@@ -2,6 +2,7 @@
@import 'variables.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 4fdd9e8..4a634e9 100644
--- a/sass/src/light/_variables.scss
+++ b/sass/src/light/_variables.scss
@@ -5,3 +5,6 @@ $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 170b750..531c346 100644
--- a/sass/src/light/index.scss
+++ b/sass/src/light/index.scss
@@ -2,6 +2,7 @@
@import 'variables.scss';
@import '../../materialize.scss';
@import '../common/index.scss';
+@import '../common/local.scss';
.progress {
background-color: color('grey', 'lighten-2');
diff --git a/scripts/asset-rebuild b/scripts/asset-rebuild
index dd59380..eb6a140 100755
--- a/scripts/asset-rebuild
+++ b/scripts/asset-rebuild
@@ -1,17 +1,15 @@
#!/bin/sh
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
set -x
-scripts/update-autocomplete
-
for theme in dark light; do
sassc -t compressed sass/src/${theme}/index.scss public/static/css/${theme}.min.css
done
-for file in autocomplete geolocation travelynx-actions; do
+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 6a20835..1f96d33 100755
--- a/scripts/asset-release
+++ b/scripts/asset-release
@@ -1,6 +1,6 @@
#!/bin/sh
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
diff --git a/scripts/update-autocomplete b/scripts/update-autocomplete
deleted file mode 100755
index 4dc64ae..0000000
--- a/scripts/update-autocomplete
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env perl
-
-use strict;
-use warnings;
-use 5.020;
-
-use JSON;
-use File::Slurp qw(write_file);
-use Travel::Status::DE::IRIS::Stations;
-
-my @station_names
- = map { $_->[1] } Travel::Status::DE::IRIS::Stations::get_stations();
-my $station_list = q{};
-
-for my $station (@station_names) {
- $station_list .= sprintf( "\t\t\t\"%s\": null,\n", $station );
-}
-
-my $autocomplete = <<"EOF";
-/*
- * Copyright (C) 2020 DB Station&Service AG, Europaplatz 1, 10557 Berlin
- * Copyright (C) 2020 Daniel Friesel
- *
- * SPDX-License-Identifier: CC-BY-4.0
- */
-document.addEventListener('DOMContentLoaded', function() {
- var elems = document.querySelectorAll('.autocomplete');
- M.Autocomplete.init(elems, {
- minLength: 3,
- limit: 50,
- data: {
-$station_list
- }
- });
-});
-EOF
-
-write_file(
- "public/static/js/autocomplete.js",
- { binmode => ':encoding(utf-8)' },
- $autocomplete
-);
diff --git a/share/ice_names.json b/share/ice_names.json
index 18bf252..be386b8 100755
--- a/share/ice_names.json
+++ b/share/ice_names.json
@@ -1,233 +1,254 @@
{
-"4712" : "Dillingen a.d. Donau",
-"4601" : "Europa/Europe",
-"1183" : "Oberursel (Taunus)",
-"363" : "Weilheim i. OB",
-"187" : "Mühldorf a. Inn",
-"101" : "Gießen",
-"331" : "Westerland/Sylt",
-"357" : "Esslingen am Neckar",
-"333" : "Goslar",
-"4717" : "Paris",
-"102" : "Jever",
-"4604" : "Brussel/Bruxelles",
-"175" : "Nürnberg",
-"173" : "Basel",
-"177" : "Rendsburg",
-"118" : "Gelnhausen",
-"320" : "Weil am Rhein",
-"186" : "Chur",
-"354" : "Mittenwald",
-"1155" : "Mühlhausen/Thüringen",
-"355" : "Tuttlingen",
-"308" : "Murnau am Staffelsee",
-"353" : "Neu-Ulm",
-"1505" : "Marburg/Lahn",
-"104" : "Fulda",
-"172" : "Aschaffenburg",
-"182" : "Rüdesheim",
-"1174" : "Hansestadt Warburg",
-"1168" : "Ellwangen",
-"180" : "Castrop-Rauxel",
-"1170" : "Prenzlau",
-"1164" : "Rödental",
-"1175" : "Villingen-Schwenningen",
-"1166" : "Bingen am Rhein",
-"327" : "Siegen",
-"362" : "Schwerte (Ruhr)",
-"1151" : "Elsterwerda",
-"360" : "Linz am Rhein",
-"1163" : "Ostseebad Binz",
-"103" : "Neu-Isenburg",
-"352" : "Mönchengladbach",
-"1167" : "Traunstein",
-"1169" : "Tutzing",
-"1162" : "Vaihingen an der Enz",
-"1157" : "Innsbruck",
-"1161" : "Andernach",
-"1158" : "Falkenberg/Elster",
-"225" : "Oldenburg (Oldb)",
-"228" : "Altenburg",
-"215" : "Bitterfeld-Wolfen",
-"359" : "Leverkusen",
-"171" : "Heusenstamm",
-"1152" : "Travemünde",
-"361" : "Celle",
-"160" : "Mülheim an der Ruhr",
-"237" : "Neustrelitz",
-"1192" : "Linz",
-"1154" : "Sonneberg",
-"154" : "Flensburg",
-"211" : "Uelzen",
-"156" : "Heppenheim/Bergstraße",
-"185" : "Freilassing",
-"323" : "Schaffhausen",
-"309" : "Aalen",
-"188" : "Hildesheim",
-"315" : "Singen (Hohentwiel)",
-"358" : "St. Ingbert",
-"335" : "Konstanz",
-"1190" : "Wien",
-"181" : "Interlaken",
-"213" : "Nauen",
-"1172" : "Bamberg",
-"183" : "Timmendorfer Strand",
-"326" : "Neunkirchen",
-"324" : "Fürth",
-"1153" : "Ilmenau",
-"157" : "Landshut",
-"1178" : "Ostseebad Warnemünde",
-"1160" : "Markt Holzkirchen",
-"222" : "Eberswalde",
-"236" : "Jüterbog",
-"4685" : "Schwäbisch Hall",
-"4684" : "Forbach-Lorraine",
-"1522" : "Torgau",
-"1182" : "Mainz",
-"1191" : "Salzburg",
-"1523" : "Hansestadt Greifswald",
-"1521" : "Homburg/Saar",
-"1181" : "Horb am Neckar",
-"1520" : "Gotha",
-"1180" : "Darmstadt",
-"1176" : "Coburg",
-"1173" : "Halle (Saale)",
-"4683" : "Limburg an der Lahn",
-"314" : "Bergisch Gladbach",
-"312" : "Montabaur",
-"1524" : "Hansestadt Rostock",
-"1184" : "Kaiserslautern",
-"169" : "Worms",
-"4680" : "Würzburg",
-"1177" : "Rathenow",
-"1156" : "Waren (Müritz)",
-"351" : "Herford",
-"1165" : "Bad Oeynhausen",
-"4682" : "Köln",
-"1171" : "Oschatz",
-"210" : "Fontanestadt Neuruppin",
-"167" : "Garmisch-Partenkirchen",
-"106" : "Itzehoe",
-"174" : "Zürich",
-"107" : "Plattling",
-"162" : "Geisenheim/Rheingau",
-"201" : "Rheinsberg",
-"117" : "Hof",
-"220" : "Meiningen",
-"217" : "Bergen auf Rügen",
-"238" : "Saarbrücken",
-"114" : "Friedrichshafen",
-"113" : "Frankenthal/Pfalz",
-"1111" : "Hansestadt Wismar",
-"1108" : "Berlin",
-"1128" : "Reutlingen",
-"168" : "Crailsheim",
-"230" : "Delitzsch",
-"313" : "Treuchtlingen",
-"1159" : "Passau",
-"110" : "Gelsenkirchen",
-"307" : "Oberhausen",
-"1132" : "Wittenberge",
-"227" : "Ludwigslust",
-"205" : "Zwickau",
-"108" : "Lichtenfels",
-"158" : "Gütersloh",
-"124" : "Hanau",
-"116" : "Pforzheim",
-"1117" : "Erlangen",
-"224" : "Saalfeld (Saale)",
-"243" : "Bautzen/Budyšin",
-"1131" : "Trier",
-"231" : "Brandenburg an der Havel",
-"242" : "Quedlinburg",
-"1125" : "Arnstadt",
-"325" : "Ravensburg",
-"221" : "Lübbenau/Spreewald",
-"1118" : "Plauen/Vogtland",
-"159" : "Bad Oldesloe",
-"302" : "Hansestadt Lübeck",
-"115" : "Regensburg",
-"112" : "Memmingen",
-"190" : "Ludwigshafen am Rhein",
-"241" : "Bad Hersfeld",
-"235" : "Görlitz",
-"336" : "Ingolstadt",
-"334" : "Offenburg",
-"1110" : "Naumburg (Saale)",
-"321" : "Krefeld",
-"178" : "Bremerhaven",
-"1119" : "Meißen",
-"305" : "Baden-Baden",
-"319" : "Duisburg",
-"4651" : "Amsterdam",
-"223" : "Schwerin",
-"1102" : "Neubrandenburg",
-"214" : "Hamm (Westf.)",
-"209" : "Riesa",
-"105" : "Offenbach am Main",
-"153" : "Neumünster",
-"120" : "Lüneburg",
-"184" : "Bruchsal",
-"316" : "Siegburg",
-"219" : "Hagen",
-"161" : "Bebra",
-"317" : "Recklinghausen",
-"1503" : "Altenbeken",
-"318" : "Münster (Westf.)",
-"240" : "Bochum",
-"328" : "Aachen",
-"322" : "Solingen",
-"202" : "Wuppertal",
-"1101" : "Neustadt an der Weinstraße",
-"1103" : "Paderborn",
-"234" : "Minden",
-"1109" : "Güstrow",
-"232" : "Frankfurt (Oder)",
-"119" : "Osnabrück",
-"1107" : "Pirna",
-"1113" : "Hansestadt Stralsund",
-"1501" : "Eisenach",
-"155" : "Rosenheim",
-"244" : "Koblenz",
-"4652" : "Arnhem",
-"212" : "Potsdam",
-"4603" : "Mannheim",
-"301" : "Freiburg im Breisgau",
-"310" : "Wolfsburg",
-"330" : "Göttingen",
-"1127" : "Weimar",
-"207" : "Stendal",
-"304" : "München",
-"4607" : "Hannover",
-"1105" : "Dresden",
-"1502" : "Karlsruhe",
-"1506" : "Kassel",
-"208" : "Bonn",
-"311" : "Wiesbaden",
-"1126" : "Leipzig",
-"216" : "Dessau",
-"176" : "Bremen",
-"4611" : "Düsseldorf",
-"203" : "Cottbus/Chóśebuz",
-"1504" : "Heidelberg",
-"303" : "Dortmund",
-"1130" : "Jena",
-"226" : "Lutherstadt Wittenberg",
-"206" : "Magdeburg",
-"1104" : "Erfurt",
-"332" : "Augsburg",
-"1129" : "Kiel",
-"239" : "Essen",
-"337" : "Stuttgart",
-"233" : "Ulm",
-"204" : "Bielefeld",
-"218" : "Braunschweig",
-"1112" : "Freie und Hansestadt Hamburg",
-"4610" : "Frankfurt am Main",
-"9006" : "Martin Luther",
-"9018" : "Freistaat Bayern",
-"9025" : "Nordrhein-Westfalen",
-"9026" : "Zürichsee",
-"152" : "Hanau",
-"166" : "Gelnhausen"
+"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
index b36d9d9..60a0d6f 100755
--- a/share/old_station_names.json
+++ b/share/old_station_names.json
@@ -1,92 +1,136 @@
{
-"Sylbach":"Bad Salzuflen-Sylbach",
-"Neubeckum":"Beckum-Neubeckum",
-"Berlin Gehrenseestr.":"Berlin Gehrenseestraße",
-"Berlin Betriebsbf Schöneweide":"Berlin-Johannisthal",
-"Brackwede":"Bielefeld-Brackwede",
-"Sennestadt":"Bielefeld-Sennestadt",
-"Windelsbleiche":"Bielefeld-Windelsbleiche",
-"Europapl./PostGalerie (Kaiserstr), Karlsruhe":"Europaplatz/Postgal. (Kaiser), Karlsruhe",
-"Europapl./PostGalerie (Karlstr.), Karlsruhe":"Europaplatz/Postgalerie (Karlstr.), Karlsruhe",
-"Europaplatz/Postgalerie (Karls, Karlsruhe":"Europaplatz/Postgalerie (Karlstr.), Karlsruhe",
-"Halle(W) Gerry-Weber-Stadion":"Halle(Westf) OWL-Arena",
-"Dingden":"Hamminkeln-Dingden",
-"Galgenschanze":"Kaiserslautern Galgenschanze",
-"Sarnau":"Lahntal-Sarnau",
-"Lindau Hbf":"Lindau-Insel",
-"Waggonfabrik":"Mainz Waggonfabrik",
-"Mook Molenhoek":"Mook-Molenhoek",
-"Nancy Ville":"Nancy",
-"Neuenbürg(Enz) Eyachbrücke":"Neuenbürg(Enz)-Rotenbach Eyachbrücke",
-"Neumarkt-Köstendorf":"Neumarkt/Wallersee",
-"Sennelager":"Paderborn-Sennelager",
-"Bösensell":"Senden-Bösensell",
-"St. Margrethen":"St. Margrethen SG",
-"Blumenkamp":"Wesel-Blumenkamp",
-"Wilhelmshaven Hbf":"Wilhelmshaven",
-"Pfeddersheim":"Worms-Pfeddersheim",
-"Wusterhausen(Dosse) NE":"Wusterhausen(Dosse)",
-"Berlin-Schönefeld Flughafen":"Flughafen BER - Terminal 5 (Schönefeld)",
-"OberurselWeißkirchen/Steinbach":"Oberursel-Weißkirchen/Steinbach",
-"Amersfoort":"Amersfoort Centraal",
-"Bernburg":"Bernburg Hbf",
-"Binsfeld":"Nörvenich-Binsfeld",
-"Jakobwüllesheim":"Vettweiß-Jakobwüllesheim",
-"Krumbach(Schw)Schule":"Krumbach(Schwab)Schule",
-"Nordbögge":"Bönen-Nordbögge",
-"Hamm(Westf)":"Hamm(Westf)Hbf",
-"Cottbus":"Cottbus Hbf",
-"Delft Zuid":"Delft Campus",
-"Barchel, Oerel":"Barchel",
-"Biedenkopf-Schulzentrum":"Biedenkopf Campus",
-"Bruchsal Tunnelstr":"Bruchsal Tunnelstraße",
-"Einbeck Salzderhelden":"Einbeck-Salzderhelden",
-"Eindhoven":"Eindhoven Centraal",
-"Escherndorf-Vogelburg":"Escherndorf-Vogelsburg",
-"Essel, Kutenholz":"Essel",
-"Europapl./Postgalerie (Karl), Karlsruhe":"Europaplatz/Postgalerie (Karls, Karlsruhe",
-"Furth i Wald":"Furth im Wald",
-"Germersheim Bahnhof":"Germersheim",
-"Glossen (b Oschatz)":"Glossen(b Oschatz)",
-"Gondelsheim Schloßstadion":"Gondelsheim Schlossstadion",
-"Hagen, Stade":"Hagen(Kr. Stade)",
-"Holzgerlingen Nord":"Holzgerlingen Hülben",
-"Karlsruhe Albtalbf":"Karlsruhe Albtalbahnhof",
-"Karlsruhe Durlacher Tor":"Karlsruhe Durlacher Tor / KIT-Campus Süd",
-"Karlsruhe Mühlburger Tor":"Karlsruhe Mühlburger Tor (Kaiserallee)",
-"Korbach":"Korbach Hbf",
-"Merseburg":"Merseburg Hbf",
-"Münster(b Dieburg)":"Münster(Hessen)",
-"Neu Isenburg":"Neu-Isenburg",
-"Niebüll, Sylt Shuttle":"Niebüll Autoverladung",
-"Olen":"Olen(Belgien)",
-"Rahden(Kr Lübbecke)":"Rahden",
-"Riegel-Malterd.NE":"Riegel-Malterdingen NE",
-"Siegen":"Siegen Hbf",
-"Stendal":"Stendal Hbf",
-"Teisnach Rohde&Schwarz":"Teisnach Rohde+Schwarz",
-"Thalheim (b Oschatz)":"Thalheim(b Oschatz)",
-"Timmendorferstrand":"Timmendorfer Strand",
-"Waldkraiburg":"Waldkraiburg-Kraiburg",
-"Weinheim(Bergstr)":"Weinheim(Bergstr)Hbf",
-"Werningerode":"Weringerode Hbf",
-"Westerland(Sylt), Sylt Shuttle":"Westerland (Sylt) Autoverladung",
-"Stryck":"Willingen-Stryck",
-"Rudersdorf(Siegen)":"Wilnsdorf-Rudersdorf",
-"Holzhausen-Heddinghausen":"Bad Holzhausen",
-"Hummelberg":"Berghausen Hummelberg",
-"Wehrden":"Beverungen-Wehrden",
-"Bockum-Hövel":"Hamm-Bockum-Hövel",
-"Brügge(Westf)":"Lüdenscheid-Brügge",
-"Dieringhausen":"Gummersbach-Dieringhausen",
-"Eisenbach-Matzenbach":"Matzenbach",
-"Godelheim":"Höxter-Godelheim",
-"Heessen":"Hamm-Heessen",
-"Lüchtringen":"Höxter-Lüchtringen",
-"Ottbergen":"Höxter-Ottbergen",
-"Preußen":"Lünen-Preußen",
-"Rudersdorf(Siegen)":"Wilnsdorf-Rudersdorf",
-"St Augustin Markt":"Sankt Augustin Zentrum",
-"Untersulzbach":"Sulzbachtal",
-"Freiburg West":"Freiburg-Landwasser"
+ "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 bbdacf7..3727f1e 100644
--- a/t/01-static.t
+++ b/t/01-static.t
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
diff --git a/t/02-registration.t b/t/02-registration.t
index cd2201a..799022f 100644
--- a/t/02-registration.t
+++ b/t/02-registration.t
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
@@ -33,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
@@ -62,7 +63,7 @@ $t->post_ok(
password2 => 'foofoofoo',
}
);
-$t->status_is(200)->content_like(qr{CSRF});
+$t->status_is(400)->content_like(qr{CSRF});
# Failed registration (user name not available)
$t->post_ok(
@@ -88,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};
@@ -108,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(
diff --git a/t/11-journey-stats.t b/t/11-journey-stats.t
index e50c5b6..9853b85 100644
--- a/t/11-journey-stats.t
+++ b/t/11-journey-stats.t
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
@@ -33,6 +33,7 @@ $t->app->pg->on(
$t->app->config->{mail}->{disabled} = 1;
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
$t->app->start( 'database', 'migrate' );
my $csrf_token
diff --git a/t/12-journey-edit.t b/t/12-journey-edit.t
index 1f00b85..27e309b 100644
--- a/t/12-journey-edit.t
+++ b/t/12-journey-edit.t
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
@@ -35,6 +35,7 @@ $t->app->pg->on(
$t->app->config->{mail}->{disabled} = 1;
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
$t->app->start( 'database', 'migrate' );
my $csrf_token
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
index 7abe1bc..78bd6e0 100644
--- a/t/r-negative-delay.t
+++ b/t/r-negative-delay.t
@@ -1,6 +1,6 @@
#!/usr/bin/env perl
-# Copyright (C) 2020 Daniel Friesel <daniel.friesel@uos.de>
+# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org>
#
# SPDX-License-Identifier: MIT
@@ -34,6 +34,7 @@ $t->app->pg->on(
$t->app->config->{mail}->{disabled} = 1;
+$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 0;
$t->app->start( 'database', 'migrate' );
my $csrf_token
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
index 220c49d..79492a5 100644
--- a/templates/_cancelled_departure.html.ep
+++ b/templates/_cancelled_departure.html.ep
@@ -2,12 +2,12 @@
<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_ds100} %>"><%= $journey->{dep_name} %></a>
+ 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 = get_connecting_trains(eva => $journey->{dep_eva}, destination_name => $journey->{arr_name})) {
+ % if (my @connections = @{stash('connections_iris') // []}) {
<p>Alternative Reisemöglichkeiten:</p>
- %= include '_connections', connections => \@connections, checkin_from => $journey->{dep_ds100};
+ %= 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 76d11bc..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')->iso8601 %>?station=<%= $journey->{dep_eva} %>">
+ <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;
% }
@@ -171,9 +187,12 @@
Bitte wähle ein neues Reiseziel.
</p>
% }
- % if (@{$journey->{messages} // []} or @{$journey->{extra_data}{qos_msg} // []}) {
+ % 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>
@@ -187,51 +206,64 @@
</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" aria-hidden="true">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" aria-hidden="true">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) {
@@ -240,33 +272,48 @@
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}) {
+ % 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} %> #travelynx"
+ 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" 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: 3em;">
+ <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} // []}) {
@@ -278,10 +325,30 @@
</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://marudor.de/details/' . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . DateTime->now(time_zone => 'Europe/Berlin')->epoch . '000';
- <a style="margin-right: 0;" href="<%= $url %>" aria-label="Zuglauf"><i class="material-icons left">timeline</i> Zuglauf</a>
+ % 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>
% }
@@ -291,31 +358,45 @@
<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;">
@@ -323,5 +404,14 @@
</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 d09f0c0..1dd2718 100644
--- a/templates/_connections.html.ep
+++ b/templates/_connections.html.ep
@@ -1,96 +1,65 @@
-<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
- % }
- <br/>
- % if ($train->{message_id}{96} or $train->{message_id}{97}) {
- <i class="material-icons tiny" aria-label="Zug ist überbesetzt">warning</i>
- % }
- % if ($train->{message_id}{82} or $train->{message_id}{85}) {
- <i class="material-icons tiny" aria-label="Fehlende Wagen">people</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>
- <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)
- % }
- % if ($train->{interchange_icon}) {
- <i class="material-icons tiny" aria-label="<%= $train->{interchange_text} %>"><%= $train->{interchange_icon} %></i>
- % }
- % if ($train->platform) {
- <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}
+ % }
+ <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>
+ % }
+ % if ($train->{interchange_icon}) {
+ <i class="material-icons tiny" aria-label="<%= $train->{interchange_text} %>"><%= $train->{interchange_icon} %></i>
% }
% if ($train->{message_id}{96} or $train->{message_id}{97}) {
<i class="material-icons tiny" aria-label="Zug ist überbesetzt">warning</i>
% }
% if ($train->{message_id}{82} or $train->{message_id}{85}) {
- <i class="material-icons tiny" aria-label="Fehlende Wagen">people</i>
+ <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>
@@ -101,28 +70,7 @@
% if ($train->{message_id}{70} or $train->{message_id}{71}) {
<i class="material-icons tiny" aria-label="Ohne WLAN">portable_wifi_off</i>
% }
- </td>
- <td>
- % 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)
- % }
- % if ($train->{interchange_icon}) {
- <i class="material-icons tiny" aria-label="<%= $train->{interchange_text} %>"><%= $train->{interchange_icon} %></i>
- % }
- % }
- </td><td>
- % if ($train->platform and not $train->departure_is_cancelled) {
- Gleis <%= $train->platform %>
- % }
- % elsif ($train->departure_is_cancelled) {
- fällt aus
- % }
- </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 1ccb63d..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="/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>
- </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_stats.html.ep b/templates/_history_stats.html.ep
index d6c7979..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>
@@ -40,11 +13,19 @@
<tr>
<th scope="row">Fahrtzeit</th>
<td><%= $stats->{min_travel_real_strf} %> Stunden
- (nach Fahrplan: <%= $stats->{min_travel_sched_strf} %>)<td>
+ (nach Fahrplan: <%= $stats->{min_travel_sched_strf} %>)</td>
</tr>
<tr>
<th scope="row">Wartezeit (nur Umstiege)</th>
<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>
diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep
index 74dfe9e..cf998ab 100644
--- a/templates/_history_trains.html.ep
+++ b/templates/_history_trains.html.ep
@@ -1,39 +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};
- % if (my $prefix = stash('link_prefix')) {
- % $detail_link = $prefix . $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')
@@ -48,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/_invalid_input.html.ep b/templates/_invalid_input.html.ep
index 6b0fb65..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>
% }
diff --git a/templates/_map.html.ep b/templates/_map.html.ep
index 19ea617..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>
diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep
index 907427f..b463d15 100644
--- a/templates/_public_status_card.html.ep
+++ b/templates/_public_status_card.html.ep
@@ -1,35 +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>
- % if ($journey->{train_line}) {
- <div class="center-align"><b><%= $journey->{train_type} %> <%= $journey->{train_line} %></b> <%= $journey->{train_no} %></div>
- % }
- % else {
- <div class="center-align"><b><%= $journey->{train_type} %> <%= $journey->{train_no} %></b></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
@@ -79,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;
% }
@@ -104,21 +118,21 @@
% 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;
% }
@@ -147,35 +161,43 @@
</p>
% }
</div>
- <div class="card-action">
- % my $url = 'https://marudor.de/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>
+ % 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>
- % if ($journey->{arr_name}) {
- Zuletzt gesehen
- % if ($journey->{real_arrival}->epoch and ($public_level & 0x20 or ($public_level & 0x10 and is_user_authenticated()))) {
- %= $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>
- % }
+ % 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 {
- Noch keine Zugfahrten geloggt.
+ 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/about.html.ep b/templates/about.html.ep
index bced6b6..ea86bdf 100644
--- a/templates/about.html.ep
+++ b/templates/about.html.ep
@@ -1,11 +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/>
+ 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/>
- Backend:
+ 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,
@@ -15,7 +17,7 @@
<div class="row">
<div class="col s12 m12 l4 center-align" style="margin-top: 1em;">
- <a href="https://finalrewind.org/me/" 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;">
% if (my $issue_url = app->config->{ref}{issues}) {
diff --git a/templates/account.html.ep b/templates/account.html.ep
index 5e30c77..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
% }
@@ -19,15 +19,24 @@
% 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>
@@ -35,8 +44,8 @@
% }
% my $acc = current_user();
-% my $hook = get_webhook();
-% my $traewelling = traewelling->get($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">
@@ -67,35 +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 mit Anmeldung)
- % }
- % elsif ($acc->{is_public} & 0x02) {
- Aktueller Status
- % }
- % if ($acc->{is_public} & 0x0f and $acc->{is_public} & 0xf0) {
- <br/>
+ <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>
% }
- % if ($acc->{is_public} & 0x30) {
- % if ($acc->{is_public} & 0x40) {
- Vergangene Fahrten
- % }
- % else {
- Fahrten der letzten vier Wochen
- % }
- % if ($acc->{is_public} & 0x10) {
- (nur mit Anmeldung)
+ % 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) {
- <br/>
- Kommentare
+ % else {
+ <span style="color: #999999;">Accounts können dir nicht folgen</span>
% }
</td>
</tr>
@@ -117,30 +122,44 @@
% }
</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">Träwelling</th>
+ <th scope="row">Externe Dienste</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>
+ <a href="/account/services"><i class="material-icons">edit</i></a>
+ % if ($acc->{sb_name}) {
+ Abfahrtstafel: <%= $acc->{sb_name} %>
% }
% 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
- % }
+ <span style="color: #999999;">Keine</span>
% }
</td>
</tr>
@@ -162,7 +181,93 @@
</div>
</div>
-% 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>
@@ -170,7 +275,7 @@
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>
@@ -271,14 +376,6 @@
<div class="row">
<div class="col s12">
- <a href="/api">Dokumentation</a>
- </div>
-</div>
-
-
-
-<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>
diff --git a/templates/add_journey.html.ep b/templates/add_journey.html.ep
index 78d70d1..c543781 100644
--- a/templates/add_journey.html.ep
+++ b/templates/add_journey.html.ep
@@ -1,4 +1,4 @@
-<h1>Zugfahrt eingeben</h1>
+<h1>Fahrt eingeben</h1>
% if (not journeys->get_oldest_ts(uid => current_user->{id})) {
<div class="row">
<div class="col s12">
@@ -6,8 +6,10 @@
<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,9 +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 Zugnummer nicht bekannt: einen beliebigen Integer eintragen, z.B. "S 5X 0"</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>
@@ -40,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>
diff --git a/templates/api_documentation.html.ep b/templates/api_documentation.html.ep
index 55cd54a..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,10 +27,12 @@
<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/>
@@ -43,7 +41,7 @@
},<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/>
@@ -61,12 +59,16 @@
…<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>
@@ -82,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/>
@@ -115,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/>
@@ -160,7 +175,7 @@
<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
@@ -178,18 +193,18 @@
<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/>
+ "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, (Zugausfall?)<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/>
@@ -209,7 +224,7 @@
{<br/>
"success" : true,<br/>
"deprecated" : true / false, (falls true: Diese API-Version wird irgendwann abgeschaltet, bitte auf eine neue umsteigen)<br/>
- "id" : 1234, (ID der eingetragenen Zugfahrt)<br/>
+ "id" : 1234, (ID der eingetragenen Fahrt)<br/>
"result" : { ... } (Eingetragene Daten. Das Datenformat kann sich
ohne Berücksichtigung der API-Version ändern)<br/>
}
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_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 9223a62..09126a8 100644
--- a/templates/changelog.html.ep
+++ b/templates/changelog.html.ep
@@ -2,6 +2,403 @@
<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">
@@ -19,7 +416,6 @@
</div>
</div>
-
<div class="row">
<div class="col s12 m1 l1">
1.20
@@ -109,7 +505,7 @@
href="/account/privacy">Privatsphäre-Einstellungen</a> aktiv ist.
</p>
<p>
- <i class="material-icons left" aria-label="Bugfix">star</i>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
Behandlung von Haltausfällen während der Reise bzw. nach dem Checkin.
</p>
</div>
@@ -326,5 +722,3 @@
</ul>
</div>
</div>
-
-%= include '_footer', version => stash('version')
diff --git a/templates/commute.html.ep b/templates/commute.html.ep
index 4e575e6..26b2fbc 100644
--- a/templates/commute.html.ep
+++ b/templates/commute.html.ep
@@ -1,7 +1,7 @@
<div class="row">
<div class="col s12">
<p>
- Hier werden nur Zugfahrten angezeigt, deren Start- oder Zielstation
+ 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.
@@ -57,7 +57,7 @@
<div class="col s12 m12 l12">
<p>
An <b><%= $total_journeys %></b> Tagen im Jahr wurde mindestens
- eine Zugfahrt von oder zu
+ eine Fahrt von oder zu
% if (param('filter_type') eq 'exact') {
der ausgewählten Station
% }
diff --git a/templates/departures.html.ep b/templates/departures.html.ep
index 91b3331..6aac482 100644
--- a/templates/departures.html.ep
+++ b/templates/departures.html.ep
@@ -1,113 +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="<%= $eva %>" 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->{cancellation} and $station eq $status->{cancellation}{dep_name}) {
+% elsif ($user_status->{cancellation} and $station eq $user_status->{cancellation}{dep_name}) {
<div class="row">
<div class="col s12">
- %= include '_cancelled_departure', journey => $status->{cancellation};
+ %= include '_cancelled_departure', journey => $user_status->{cancellation};
</div>
</div>
% }
-% elsif ($status->{timestamp_delta} < 180) {
+% elsif ($user_status->{timestamp_delta} < 180) {
<div class="row">
<div class="col s12">
- %= include '_checked_out', journey => $status;
+ %= include '_checked_out', journey => $user_status;
</div>
</div>
% }
-% elsif (not param('train') and my @connections = get_connecting_trains(eva => $eva)) {
+% 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 => $eva;
+ <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) %>)
- % }
- % 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>
- % }
- </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 69c5365..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>
@@ -43,7 +43,7 @@
</p>
<table class="striped">
<tr>
- <th scope="row">Zug</th>
+ <th scope="row">Fahrt</th>
<td>
<%= $journey->{type} %> <%= $journey->{no} %>
% if ($journey->{line}) {
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 6b8c335..71d180f 100644
--- a/templates/history.html.ep
+++ b/templates/history.html.ep
@@ -18,7 +18,7 @@ Für Details ein Jahr auswählen.
<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" aria-hidden="true">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">
diff --git a/templates/history_by_year.html.ep b/templates/history_by_year.html.ep
index a112258..6aa0c2d 100644
--- a/templates/history_by_year.html.ep
+++ b/templates/history_by_year.html.ep
@@ -3,9 +3,34 @@
% 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 06429f7..57ba81f 100644
--- a/templates/history_map.html.ep
+++ b/templates/history_map.html.ep
@@ -1,10 +1,31 @@
<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>
@@ -13,38 +34,74 @@
%= form_for '/history/map' => begin
<p>
- Detailgrad und Filter:
+ Detailgrad:
</p>
<div class="row">
<div class="input-field col s12">
- <label>
- %= radio_button route_type => 'polyline'
- <span>Nur Zugfahrten mit bekanntem Streckenverlauf eintragen</span>
- </label>
+ <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>
- %= radio_button route_type => 'polybee'
- <span>Streckenverlauf wenn bekannt, sonst Luftlinie zwischen Unterweghalten</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">
- <label>
- %= radio_button route_type => 'beeline'
- <span>Immer Luftlinie zwischen Unterwegshalten zeigen</span>
- </label>
+ %= 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">
- <label>
- %= check_box include_manual => 1
- <span>Manuelle Einträge ohne Unterwegshalte mitberücksichtigen</span>
- </label>
+ %= text_field 'filter_type', id => 'filter_type'
+ <label for="filter_tpye">Verkehrsmittel</label>
</div>
</div>
<div class="row">
@@ -69,7 +126,7 @@
<div class="row">
<div class="col s12">
<p>
- Die folgenden Zugfahrten wurden nicht eingezeichnet:
+ Die folgenden Fahrten wurden nicht eingezeichnet:
</p>
<p>
<ul>
diff --git a/templates/journey.html.ep b/templates/journey.html.ep
index a5e04a0..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>
@@ -38,13 +38,23 @@
% }
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
@@ -59,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}) {
@@ -113,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 {
?
@@ -132,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>
@@ -198,6 +208,7 @@
<tr>
<th scope="row">Route</th>
<td>
+ % my $before = 1;
% my $within = 0;
% my $at_startstop = 0;
% for my $station (@{$journey->{route}}) {
@@ -210,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>
@@ -219,6 +238,19 @@
% if ($journey->{edited} & 0x0010) {
% }
+ % 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>
@@ -230,6 +262,25 @@
%= 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"
diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep
index fa9bf8a..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,17 +48,20 @@
</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>
<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', autocomplete => 'off', required => undef
- <label for="station">Manuelle Eingabe (Name oder DS100)</label>
+ <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">
@@ -69,8 +75,8 @@
% }
</div>
</div>
- <h1>Letzte Fahrten</h1>
- %= include '_history_trains', date_format => '%d.%m', journeys => [journeys->get(uid => current_user->{id}, 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,11 @@
<ul>
<li>Protokoll von Fahrplan- und Echtzeitdaten an Start- und
Zielbahnhof</li>
+ <li>Teilen von aktuellen und vergangenen Fahrten mit anderen Personen</li>
<li>Web-Hooks und <a href="/api">API</a> zum automatisierten Einchecken und Auslesen des aktuellen Status</li>
<li>Statistiken über Reisezeiten und Verspätungen</li>
<li>Unterstützung beim Ausfüllen von Fahrgastrechteformularen</li>
- <li>Optional: Öffentliches Profil und Reisestatus</li>
+ <li>Optional: Öffentlicher Reisestatus und öffentliche Angaben zu vergangenen Fahrten</li>
<li>Optional: Verknüpfung mit Träwelling</li>
</ul>
</p>
@@ -121,5 +128,3 @@
</div>
</div>
% }
-
-%= include '_footer', version => stash('version')
diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep
index d029251..fbb26ef 100644
--- a/templates/layouts/default.html.ep
+++ b/templates/layouts/default.html.ep
@@ -13,14 +13,14 @@
% while (my ($key, $value) = each %{stash('opengraph') // {}}) {
<meta property="og:<%= $key %>" content="<%= $value %>">
% }
- % my $av = 'v38'; # asset version
+ % 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'
@@ -46,31 +46,29 @@
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">
@@ -91,12 +89,9 @@
</div>
</div>
</li>
- <li class="waves-effect waves-light">
- <a onClick="javascript:toggleTheme()" title="Farbschema invertieren"><i class="material-icons" aria-label="Farbschema invertieren">invert_colors</i></a>
- </li>
- % if (is_user_authenticated()) {
+ % 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">account_circle</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="Über Travelynx"><i class="material-icons" aria-label="Über Travelynx">info_outline</i></a></li>
@@ -117,16 +112,43 @@
</div>
% }
+% if (app->config->{announcement}) {
+<div class="container">
+ <div class="row">
+ <div class="col s12 caution-color white-text">
+ %= app->config->{announcement}
+ </div>
+ </div>
+</div>
+% }
+
<div class="container">
- % if (is_user_authenticated()) {
- % my $acc = current_user();
- % if ($acc and $acc->{deletion_requested}) {
- %= include '_deletion_note', timestamp => $acc->{deletion_requested}
- % }
+ % 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 b5f3bb3..8cd3a78 100644
--- a/templates/privacy.html.ep
+++ b/templates/privacy.html.ep
@@ -1,68 +1,140 @@
<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
%= csrf_field
-<h2>Aktueller Status</h2>
+<h2>Fahrten</h2>
<div class="row">
<div class="input-field col s12">
<div>
- <label>
- %= radio_button status_level => 'private'
- <span>Nicht sichtbar</span>
- </label>
- </div><div>
- <label>
- %= radio_button status_level => 'intern'
- <span>Nur mit Anmeldung</span>
- </label>
- </div><div>
- <label>
- %= radio_button status_level => 'extern'
- <span>Öffentlich</span>
- </label>
+ <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">
+ <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">
- Hier kannst du auswählen, ob dein aktueller Status unter <a
- href="/status/<%= $name %>">/status/<%= $name %></a> sowie <a
- href="/p/<%= $name %>">/p/<%= $name %></a> abrufbar ist.
- Wenn du eingecheckt bist, werden dort Zug, Start- und Zielstation
- sowie Abfahrts- und Ankunftszeit gezeigt, andernfalls lediglich der
- Zielbahnhof der letzten Reise. Wann die letzte Reise beendet wurde,
- wird nur angegeben, wenn deine vergangenen Zugfahrten sichtbar sind
- (siehe unten).
+ 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 Zugfahrten</h2>
+<h2>Vergangene Fahrten</h2>
<div class="row">
- <div class="input-field col s12 m6 l6">
+ <div class="input-field col s12">
<div>
- <label>
- %= radio_button history_level => 'private'
- <span>Nicht sichtbar</span>
- </label>
- </div><div>
- <label>
- %= radio_button history_level => 'intern'
- <span>Nur mit Anmeldung</span>
- </label>
- </div><div>
- <label>
- %= radio_button history_level => 'extern'
- <span>Öffentlich</span>
- </label>
+ <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 class="input-field col s12 m6 l6">
+ </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'
@@ -78,14 +150,11 @@
</div>
<div class="row">
<div class="col s12">
- Diese Einstellung bestimmt die Sichtbarkeit deiner vergangenen
- Zugfahrten mit allen dazu bekannten Details (Abfahrt, Ankunft,
- Wagenreihung u.a.). Bis zu zehn deiner Fahrten werden unter <a
- href="/p/<%= $name %>">/p/<%= $name %></a> aufgelistet und verlinkt,
- dort nicht eingetragene Fahrten sind jedoch weiterhin über /p/<%=
- $name %>/j/ID zugänglich. Da die ID (mit Lücken) aufsteigend vergeben
- wird, sind effektiv alle deiner vergangenen Fahrten (oder alle Fahrten
- der letzten vier Wochen) öffentlich.
+ 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>
@@ -100,7 +169,8 @@
<div class="row">
<div class="col s12">
Wenn aktiv, sind von dir eingetragene Freitext-Kommentare in deinem
- aktuellen Status sowie bei deinen vergangenen Zugfahrten sichtbar.
+ 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
index 6a8d67d..6f78ea0 100644
--- a/templates/profile.html.ep
+++ b/templates/profile.html.ep
@@ -10,20 +10,83 @@
</div>
</div>
% }
-% if ($public_level & 0x02 or ($public_level & 0x01 and is_user_authenticated())) {
- <div class="row">
- <div class="col s12 publicstatuscol" data-user="<%= $name %>">
- %= include '_public_status_card', name => $name, public_level => $public_level, journey => $journey
+<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>
-% }
-% if ($public_level & 0x20 or ($public_level & 0x10 and is_user_authenticated())) {
+</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>Letzte Fahrten von <%= $name %></h1>
+ <h2>Vergangene Fahrten</h2>
</div>
</div>
%= include '_history_trains', date_format => '%d.%m.%Y', link_prefix => "/p/${name}/j/", journeys => $journeys;
% }
-
-%= include '_footer', version => stash('version')
diff --git a/templates/register.html.ep b/templates/register.html.ep
index c27b591..ee344f9 100644
--- a/templates/register.html.ep
+++ b/templates/register.html.ep
@@ -17,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>
@@ -47,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
@@ -62,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
index 7c38de1..c1f2b7d 100644
--- a/templates/traewelling.html.ep
+++ b/templates/traewelling.html.ep
@@ -4,39 +4,19 @@
<h1>Träwelling</h1>
-<div class="row">
- <div class="col s12">
- <div class="card purple">
- <div class="card-content white-text">
- <span class="card-title">Beta-Feature</span>
- <p>Die Verbindung von Checkinservices bietet viele Möglichkeiten für interessante Fehlerbilder.
- Falls etwas nicht klappt, bitte mit möglichst detaillierten Angaben zum Hergang einen Bug melden.
- Bekannte Einschränkung: Hooks werden bei einem Checkin via Träwelling 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 (stash('new_traewelling')) {
+% 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} // $traewelling->{email};
- <p>Dein travelynx-Account hat nun ein Jahr lang Zugriff auf
- den Träwelling-Account <b>@<%= $user %></b>.</p>
+ % 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 = stash('login_error')) {
+ % 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>
@@ -49,7 +29,7 @@
<div class="card-content white-text">
<span class="card-title">Logout-Fehler</span>
<p>Der Logout bei Träwelling ist fehlgeschlagen: <%= $logout_err %>.
- Dein Login-Token bei travelynx wurde dennoch gelöscht, so
+ 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>
@@ -61,16 +41,40 @@
</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">Login-Token abgelaufen</span>
+ <span class="card-title">Token abgelaufen</span>
% }
% else {
- <span class="card-title">Login-Token läuft bald ab</span>
+ <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,
@@ -96,37 +100,20 @@
<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 von Checkins
- zwischen den beiden Diensten. Träwelling-Checkins in
- Nahverkehrsmittel und Züge außerhalb des deutschen Schienennetzes
- werden nicht unterstützt und ignoriert.
- </p>
- <p>
- Mit E-Mail und Passwort wird ein Login über die Träwelling-API
- durchgeführt. Die E-Mail und das dabei generierte Token werden
- von travelynx gespeichert. Das Passwort wird ausschließlich für
- den Login verwendet und nicht gespeichert. Der Login kann jederzeit
- sowohl auf dieser Seite als auch über die <a
- href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
- widerrufen werden. Nach einem Jahr läuft er automatisch ab.
+ 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 '/account/traewelling' => (method => 'POST') => begin
+ %= form_for '/oauth/traewelling' => (method => 'POST') => begin
%= csrf_field
- <div class="input-field col s12">
- <i class="material-icons prefix">account_circle</i>
- %= text_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250
- <label for="email">E-Mail</label>
- </div>
- <div class="input-field col s12">
- <i class="material-icons prefix">lock</i>
- %= password_field 'password', id => 'password', class => 'validate', required => undef
- <label for="password">Passwort</label>
- </div>
<div class="col s12 center-align">
- <button class="btn waves-effect waves-light" type="submit" name="action" value="login">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="connect">
Verknüpfen
<i class="material-icons right">send</i>
</button>
@@ -140,12 +127,12 @@
<p>
Dieser travelynx-Account ist mit dem Träwelling-Account
% if (my $user = $traewelling->{data}{user_name}) {
- <a href="https://traewelling.de/profile/<%= $user %>"><%= $user %></a>
+ <a href="https://traewelling.de/@<%= $user %>"><%= $user %></a>
% }
% else {
%= $traewelling->{email}
% }
- verknüpft. Der Login-Token läuft <%= $traewelling->{expires_on}->strftime('am %d.%m.%Y um %H:%M Uhr') %> ab.
+ verknüpft. Der aktuelle Token läuft <%= $traewelling->{expires_on}->strftime('am %d.%m.%Y um %H:%M Uhr') %> ab.
</p>
</div>
</div>
@@ -180,7 +167,11 @@
</label>
</div>
<p>Die Synchronisierung erfolgt spätestens drei Minuten nach der
- Zielwahl. Träwelling-Checkins können von travelynx noch nicht
+ 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
@@ -194,11 +185,12 @@
<span>Checkin-Synchronisierung Träwelling → travelynx</span>
</label>
</div>
- <p>Alle drei Minuten wird dein Status auf Träwelling abgefragt.
- Falls du gerade in einen Zug eingecheckt bist, wird dieser von
- travelynx übernommen. Träwelling-Checkins in Nahverkehrsmittel
- und Züge außerhalb des deutschen Schienennetzes werden nicht
- unterstützt.</p>
+ <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">
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 7d543bb..280ba27 100644
--- a/templates/webhooks.html.ep
+++ b/templates/webhooks.html.ep
@@ -88,12 +88,12 @@
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>
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>