diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b7aecf8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+*.orig
+*~
+*#*
+
+ABOUT-NLS
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache/
+libtool
+config/
+config.log
+config.status
+configure
+install-sh
+missing
+.deps/
+.flatpak-builder/
+.libs/
+*.o
+*.lo
+*.la
+
+data/org.gnome.Maps.appdata.xml
+data/org.gnome.Maps.desktop
+data/org.gnome.Maps.gschema.valid
+
+m4/*.m4
+
+po/.intltool-merge-cache
+po/Makefile.in.in
+po/Makevars.template
+po/POTFILES
+po/Rules-quot
+po/stamp-it
+po/*.sed
+po/*.gmo
+po/*.sin
+po/*.header
+
+src/gnome-maps
+src/*.gresource
+src/org.gnome.Maps.service
+
+/lib/GnomeMaps-1.0.gir
+/lib/GnomeMaps-1.0.typelib
+/lib/maps-enum-types.[ch]
+
+build/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..7d6be33
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,72 @@
+stages:
+- test
+- review
+
+variables:
+    # Replace with your preferred file name of the resulting Flatpak bundle
+    BUNDLE: "gnome-maps-git.flatpak"
+
+flatpak:
+    image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master
+    stage: test
+    variables:
+        # Replace with your manifest path
+        MANIFEST_PATH: "org.gnome.Maps.json"
+        RUNTIME_REPO: "https://sdk.gnome.org/gnome-nightly.flatpakrepo"
+        # Replace with your application name, as written in the manifest
+        FLATPAK_MODULE: "gnome-maps"
+        # Make sure to keep this in sync with the Flatpak manifest, all arguments
+        # are passed except the config-args because we build it ourselves
+        MESON_ARGS: ""
+        DBUS_ID: "org.gnome.Maps"
+
+    script:
+        - flatpak-builder --stop-at=${FLATPAK_MODULE} app ${MANIFEST_PATH}
+        # Make sure to keep this in sync with the Flatpak manifest, all arguments
+        # are passed except the config-args because we build it ourselves
+        - flatpak build app meson --prefix=/app ${MESON_ARGS} _build
+        - flatpak build app ninja -C _build install
+        - flatpak-builder --finish-only --repo=repo app ${MANIFEST_PATH}
+        # Generate a Flatpak bundle
+        - flatpak build-bundle repo ${BUNDLE} --runtime-repo=${RUNTIME_REPO} ${DBUS_ID}
+        # Run automatic tests inside the Flatpak env
+        - xvfb-run -a -s "-screen 0 1024x768x24" flatpak build app ninja -C _build test
+    artifacts:
+        paths:
+            - ${BUNDLE}
+            - _build/meson-logs/meson-log.txt
+            - _build/meson-logs/testlog.txt
+        expire_in: 30 days
+    cache:
+        paths:
+             - .flatpak-builder/cache
+
+review:
+    stage: review
+    dependencies:
+        - flatpak
+    script:
+        - echo "Generating flatpak deployment"
+    artifacts:
+        paths:
+            - ${BUNDLE}
+        expire_in: 30 days
+    environment:
+        name: review/$CI_COMMIT_REF_NAME
+        url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
+        on_stop: stop_review
+    except:
+        - master@GNOME/gnome-maps
+        - tags
+
+stop_review:
+    stage: review
+    script:
+        - echo "Stopping flatpak deployment"
+    when: manual
+    environment:
+        name: review/$CI_COMMIT_REF_NAME
+        action: stop
+    except:
+        - master@GNOME/gnome-maps
+        - tags
diff --git a/NEWS b/NEWS
index bef3bad..f9f7a29 100644
--- a/NEWS
+++ b/NEWS
@@ -1,23 +1,28 @@
-3.34.1 - Oct 7, 2019
+3.35.1 - Oct 12, 2019
 =========================
 
 Changes since 3.34.0
- - Update tile size to 512 px when using --local option
+ - Initial support for public trasit routing/journey planning using third-party
+   service providers
+ - Add nightly app icon (currently not installed, awaiting support for dual
+   installations)
+ - Update default tile size when using local tiles
 
 Added/updated/fixed translations
+ - Spanish
  - Danish
  - Slovak
- - Persian
  - Dutch
  - Friulian
  - Italian
 
 All contributors to this release
 Ask Hjorth Larsen <asklarsen@gmail.com>
-Danial Behzadi <dani.behzi@ubuntu.com>
+Daniel Mustieles <daniel.mustieles@gmail.com>
 Dušan Kazik <prescott66@gmail.com>
 Fabio Tomat <f.t.public@gmail.com>
 Gianvito Cavasoli <gianvito@gmx.it>
+Jakub Steiner <jimmac@gmail.com>
 Marcus Lundblad <ml@update.uu.se>
 Nathan Follens <nfollens@gnome.org>
 
diff --git a/data/icons/private/hicolor/16x16/apps/route-transit-airplane-symbolic.svg b/data/icons/private/hicolor/16x16/apps/route-transit-airplane-symbolic.svg
new file mode 100644
index 0000000..ae2d69a
--- /dev/null
+++ b/data/icons/private/hicolor/16x16/apps/route-transit-airplane-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+    <path d="M6.497 1c-.794.001-.781.033-.532 1.031L7.59 7h-4.5L1.872 5.219C1.732 5.009 1.749 5 1.528 5h-.219c-.428 0-.281.438-.281.438L1.309 8l-.281 2.563s-.14.437.25.437h.25c.211 0 .204-.009.344-.219L3.09 9h4.5l-1.625 4.938C5.704 14.983 5.701 15 6.497 15c.432 0 .433-.012.718-.5L10.903 9h3.094c.554 0 1-.446 1-1s-.446-1-1-1h-3.094L7.215 1.5c-.266-.457-.283-.498-.656-.5z" fill="#474747"/>
+</svg>
diff --git a/data/icons/public/hicolor/scalable/apps/org.gnome.Maps.Devel.svg b/data/icons/public/hicolor/scalable/apps/org.gnome.Maps.Devel.svg
new file mode 100644
index 0000000..3ecd81a
--- /dev/null
+++ b/data/icons/public/hicolor/scalable/apps/org.gnome.Maps.Devel.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><clipPath id="k"><path d="M36 12h68v85H36zm0 0"/></clipPath><clipPath id="E"><path d="M8 28h112v88H8zm0 0"/></clipPath><clipPath id="G"><path d="M8 28h24v69H8zm0 0"/></clipPath><clipPath id="H"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="I"><path d="M36 12h68v85H36zm0 0"/></clipPath><clipPath id="J"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="K"><path d="M8 12h100v89H8zm0 0"/></clipPath><clipPath id="L"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="M"><path d="M8 12h112v104H8zm0 0"/></clipPath><clipPath id="N"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="B"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="z"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="b"><path d="M0 0h192v152H0z"/></clipPath><clipPath id="w"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="f"><path d="M0 0h128v128H0z"/></clipPath><clipPath id="c"><path d="M0 0h192v152H0z"/></clipPath><clipPath id="e"><path d="M0 0h192v152H0z"/></clipPath><clipPath id="d"><path d="M0 0h192v152H0z"/></clipPath><clipPath id="p"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="g"><path d="M8 28h112v88H8zm0 0"/></clipPath><clipPath id="h"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="i"><path d="M8 28h24v69H8zm0 0"/></clipPath><clipPath id="j"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="F"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="l"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="m"><path d="M8 12h100v89H8zm0 0"/></clipPath><clipPath id="n"><path d="M16 28h16l16-16 16 16h48c4.434 0 8 3.566 8 8v72c0 4.434-3.566 8-8 8H64l-16-16-16 16H16a7.98 7.98 0 01-8-8V36c0-4.434 3.602-8.55 8-8zm0 0"/></clipPath><clipPath id="o"><path d="M8 12h112v104H8zm0 0"/></clipPath><mask id="t"><g filter="url(#a)"><path fill-opacity=".15" d="M0 0h128v128H0z"/></g></mask><mask id="O"><g filter="url(#a)"><path fill-opacity=".3" d="M0 0h128v128H0z"/></g></mask><mask id="D"><g filter="url(#a)"><path fill-opacity=".8" d="M0 0h128v128H0z"/></g></mask><mask id="P"><g filter="url(#a)"><path fill-opacity=".15" d="M0 0h128v128H0z"/></g></mask><mask id="r"><g filter="url(#a)"><path fill-opacity=".3" d="M0 0h128v128H0z"/></g></mask><mask id="T"><use xlink:href="#y"/></mask><g id="q" clip-path="url(#b)"><path d="M32 131.992l16 .016V12H32zm0 0" fill="#fff"/></g><g id="s" clip-path="url(#c)"><path d="M48 131.992l16 .016V12H48zm0 0"/></g><g id="x" clip-path="url(#f)"><g clip-path="url(#g)"><g clip-path="url(#h)"><path d="M64 96h40V28h24v96H64l-16-16-16 16H0V96h32l16-16zm0 0" fill="#cdab8f"/></g></g><g clip-path="url(#i)"><g clip-path="url(#j)"><path d="M0 95.992l32 .016V28H0zm0 0" fill="#55a7eb"/></g></g><g clip-path="url(#k)"><g clip-path="url(#l)"><path d="M36 24v68l12-12 16 16.016h36L104 92V28H64L48 12zm0 0" fill="#2ec27e"/></g></g><g clip-path="url(#m)"><g clip-path="url(#n)"><path d="M99.973 28.027v59.5c0 2.508-2.34 4.489-4.235 4.489H64L48 76.148l-9.844 9.883L38 12.016h-6V92l-32 .016v8.023h31.75L48 84l16 16.04h31.738c6.606 0 11.989-5.513 12.262-12.513v-59.5zm0 0" fill="#fff"/></g></g><g clip-path="url(#o)"><g clip-path="url(#p)"><use xlink:href="#q" mask="url(#r)"/><use xlink:href="#s" mask="url(#t)"/></g></g><path d="M92 48c6.629 0 12-5.371 12-12s-5.371-12-12-12-12 5.371-12 12 5.371 12 12 12zm0 0" fill="url(#u)"/><path d="M92 10a27.915 27.915 0 00-19.797 8.2c-10.937 10.937-10.937 28.663 0 39.6L92 77.599s13.82-13.82 19.8-19.797c10.934-10.938 10.934-28.664 0-39.602A27.93 27.93 0 0092 10zm0 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10zm0 0" fill="#a51d2d"/><path d="M92 8c-7.164 0-14.332 2.734-19.797 8.203-10.937 10.934-10.937 28.66 0 39.594L92 75.597l19.8-19.8c10.934-10.934 10.934-28.66 0-39.594A27.918 27.918 0 0092 8zm0 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10zm0 0" fill="url(#v)"/></g><g id="C" clip-path="url(#z)"><path d="M128 80.64V128H0V80.64zm0 0" fill="url(#A)"/><path d="M13.309 80.64L60.664 128H81.88l-47.36-47.36zm42.421 0L103.094 128h21.215L76.945 80.64zm42.43 0L128 110.48V89.27l-8.629-8.63zM0 88.548v21.215L18.238 128h21.215zm0 0"/></g><g id="y" clip-path="url(#w)" filter="url(#a)"><use xlink:href="#x"/></g><g id="S" clip-path="url(#B)"><use xlink:href="#C" mask="url(#D)"/></g><linearGradient id="v" gradientUnits="userSpaceOnUse" x1="336" y1="62" x2="336" y2="-180" gradientTransform="matrix(.25 0 0 .25 8 53)"><stop offset="0" stop-color="#d81d25"/><stop offset="1" stop-color="#f66151"/></linearGradient><linearGradient id="A" gradientUnits="userSpaceOnUse" x1="300" y1="235" x2="428" y2="235" gradientTransform="matrix(0 .37 -.98462 0 295.385 -30.36)"><stop offset="0" stop-color="#f9f06b"/><stop offset="1" stop-color="#f5c211"/></linearGradient><linearGradient id="u" gradientUnits="userSpaceOnUse" x1="320.5" y1="-68" x2="415.5" y2="-68" gradientTransform="matrix(0 .25263 .25263 0 109.18 -56.968)"><stop offset="0" stop-color="#a51d2d"/><stop offset="1" stop-color="#ce1921"/></linearGradient><linearGradient id="Q" gradientUnits="userSpaceOnUse" x1="320.5" y1="-68" x2="415.5" y2="-68" gradientTransform="matrix(0 .25263 .25263 0 109.18 -56.968)"><stop offset="0" stop-color="#a51d2d"/><stop offset="1" stop-color="#ce1921"/></linearGradient><linearGradient id="R" gradientUnits="userSpaceOnUse" x1="336" y1="62" x2="336" y2="-180" gradientTransform="matrix(.25 0 0 .25 8 53)"><stop offset="0" stop-color="#d81d25"/><stop offset="1" stop-color="#f66151"/></linearGradient><filter id="a" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%"><feColorMatrix in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter></defs><g clip-path="url(#E)"><g clip-path="url(#F)"><path d="M64 96h40V28h24v96H64l-16-16-16 16H0V96h32l16-16zm0 0" fill="#cdab8f"/></g></g><g clip-path="url(#G)"><g clip-path="url(#H)"><path d="M0 95.992l32 .016V28H0zm0 0" fill="#55a7eb"/></g></g><g clip-path="url(#I)"><g clip-path="url(#J)"><path d="M36 24v68l12-12 16 16.016h36L104 92V28H64L48 12zm0 0" fill="#2ec27e"/></g></g><g clip-path="url(#K)"><g clip-path="url(#L)"><path d="M99.973 28.027v59.5c0 2.508-2.34 4.489-4.235 4.489H64L48 76.148l-9.844 9.883L38 12.016h-6V92l-32 .016v8.023h31.75L48 84l16 16.04h31.738c6.606 0 11.989-5.513 12.262-12.513v-59.5zm0 0" fill="#fff"/></g></g><g clip-path="url(#M)"><g clip-path="url(#N)"><use xlink:href="#q" mask="url(#O)"/><use xlink:href="#s" mask="url(#P)"/></g></g><path d="M92 48c6.629 0 12-5.371 12-12s-5.371-12-12-12-12 5.371-12 12 5.371 12 12 12zm0 0" fill="url(#Q)"/><path d="M92 10a27.915 27.915 0 00-19.797 8.2c-10.937 10.937-10.937 28.663 0 39.6L92 77.599s13.82-13.82 19.8-19.797c10.934-10.938 10.934-28.664 0-39.602A27.93 27.93 0 0092 10zm0 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10zm0 0" fill="#a51d2d"/><path d="M92 8c-7.164 0-14.332 2.734-19.797 8.203-10.937 10.934-10.937 28.66 0 39.594L92 75.597l19.8-19.8c10.934-10.934 10.934-28.66 0-39.594A27.918 27.918 0 0092 8zm0 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10zm0 0" fill="url(#R)"/><use xlink:href="#S" mask="url(#T)"/></svg>
\ No newline at end of file
diff --git a/data/org.gnome.Maps.appdata.xml.in b/data/org.gnome.Maps.appdata.xml.in
index 35c39ed..e4a12cf 100644
--- a/data/org.gnome.Maps.appdata.xml.in
+++ b/data/org.gnome.Maps.appdata.xml.in
@@ -42,13 +42,6 @@
     </screenshot>
   </screenshots>
   <releases>
-    <release date="2019-10-07" version="3.34.1">
-      <description>
-        <ul>
-          <li>Update tile size to 512 px when using --local option</li>
-        </ul>
-      </description>
-    </release>
     <release date="2019-09-09" version="3.34.0">
       <description>
         <ul>
diff --git a/data/ui/sidebar.ui b/data/ui/sidebar.ui
index b14866b..ab7b389 100644
--- a/data/ui/sidebar.ui
+++ b/data/ui/sidebar.ui
@@ -271,7 +271,7 @@
         <child>
           <object class="GtkStack" id="linkButtonStack">
             <child>
-              <object class="GtkLinkButton" id="graphHopperLinkButton">
+              <object class="GtkLinkButton">
                 <property name="label" translatable="yes">Route search by GraphHopper</property>
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
@@ -284,7 +284,7 @@
                 </style>
               </object>
               <packing>
-                <property name="name">graphHopper</property>
+                <property name="name">turnByTurn</property>
               </packing>
             </child>
             <child>
@@ -292,16 +292,11 @@
                 <property name="visible">True</property>
                 <property name="halign">GTK_ALIGN_END</property>
                 <child>
-                  <object class="GtkLinkButton" id="openTripPlannerLinkButton">
-                    <property name="label" translatable="yes">Route search by OpenTripPlanner</property>
+                  <object class="GtkLabel" id="transitAttributionLabel">
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
                     <property name="receives_default">True</property>
-                    <property name="use_action_appearance">False</property>
-                    <property name="relief">none</property>
-                    <!-- opentripplanner.org uses an SSL cert only valid for github
-                         domains... -->
-                    <property name="uri">http://www.opentripplanner.org</property>
+                    <property name="use_markup">True</property>
                     <style>
                       <class name="small-label"/>
                     </style>
@@ -314,7 +309,7 @@
                 <child>
                   <object class="GtkMenuButton">
                     <property name="visible">True</property>
-                    <property name="popover">openTripPlannerDisclaimerPopover</property>
+                    <property name="popover">transitDisclaimerPopover</property>
                     <property name="halign">GTK_ALIGN_END</property>
                     <property name="margin-top">5</property>
                     <property name="margin-bottom">5</property>
@@ -346,7 +341,7 @@
                 </child>
               </object>
               <packing>
-                <property name="name">openTripPlanner</property>
+                <property name="name">transit</property>
               </packing>
             </child>
           </object>
@@ -354,7 +349,7 @@
       </object>
     </child>
   </template>
-  <object class="GtkPopover" id="openTripPlannerDisclaimerPopover">
+  <object class="GtkPopover" id="transitDisclaimerPopover">
     <property name="visible">False</property>
     <child>
       <object class="GtkGrid">
@@ -366,10 +361,12 @@
             <property name="margin-bottom">5</property>
             <property name="margin-start">5</property>
             <property name="margin-end">5</property>
-            <property name="label" translatable="yes">Routing itineraries for public transit is provided by GNOME
-using timetable data obtained from transit companies or agencies.
-The companies and agencies can not be held responsible for the results shown.
+            <property name="label" translatable="yes">Routing itineraries for public transit is provided by third-party
+services.
 GNOME can not guarantee correctness of the itineraries and schedules shown.
+Note that some providers might not include all available modes of transportation,
+e.g. a national provider might not include airlines, and a local provider could
+miss regional trains.
 Names and brands shown are to be considered as registered trademarks when applicable.</property>
           </object>
         </child>
diff --git a/data/ui/transit-options-panel.ui b/data/ui/transit-options-panel.ui
index 1d449cd..a11acfa 100644
--- a/data/ui/transit-options-panel.ui
+++ b/data/ui/transit-options-panel.ui
@@ -145,6 +145,13 @@
             <property name="label" translatable="yes">Ferries</property>
           </object>
         </child>
+        <child>
+          <object class="GtkCheckButton" id="airplaneCheckButton">
+            <property name="visible">True</property>
+            <property name="active">True</property>
+            <property name="label" translatable="yes">Airplanes</property>
+          </object>
+        </child>
       </object>
     </child>
   </object>
diff --git a/debian/changelog b/debian/changelog
index 5fbaa57..6791f71 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+gnome-maps (3.35.1+git20191114.4eb1d51-1) UNRELEASED; urgency=medium
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 16 Nov 2019 00:04:30 +0000
+
 gnome-maps (3.34.1-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/gnome-maps.doap b/gnome-maps.doap
index 6d5ac4d..270faed 100644
--- a/gnome-maps.doap
+++ b/gnome-maps.doap
@@ -19,20 +19,6 @@
   <bug-database
   rdf:resource="https://gitlab.gnome.org/GNOME/gnome-maps/issues" />
 
-  <maintainer>
-    <foaf:Person>
-      <foaf:name>Jonas Danielsson</foaf:name>
-      <foaf:mbox rdf:resource="mailto:jonas@threetimestwo.org" />
-      <gnome:userid>jonasdn</gnome:userid>
-    </foaf:Person>
-  </maintainer>
-  <maintainer>
-    <foaf:Person>
-      <foaf:name>Amisha Singla</foaf:name>
-      <foaf:mbox rdf:resource="mailto:amishas157@gmail.com" />
-      <gnome:userid>amishasingla</gnome:userid>
-    </foaf:Person>
-  </maintainer>
   <maintainer>
     <foaf:Person>
       <foaf:name>Marcus Lundblad</foaf:name>
diff --git a/meson.build b/meson.build
index a55155d..1694ef6 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('gnome-maps', 'c',
-	version: '3.34.1',
+	version: '3.35.1',
 	license: 'GPL2+'
 )
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 39a716a..529f516 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -39,7 +39,6 @@ src/graphHopper.js
 src/layersPopover.js
 src/mainWindow.js
 src/mapView.js
-src/openTripPlanner.js
 src/osmConnection.js
 src/osmEditDialog.js
 src/photonParser.js
@@ -59,3 +58,4 @@ src/transitOptionsPanel.js
 src/transitPlan.js
 src/translations.js
 src/utils.js
+src/transitplugins/openTripPlanner.js
diff --git a/po/cs.po b/po/cs.po
index 2e77f6f..cad0636 100644
--- a/po/cs.po
+++ b/po/cs.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gnome-maps\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-09-01 23:55+0000\n"
-"PO-Revision-Date: 2019-09-02 02:04+0200\n"
+"POT-Creation-Date: 2019-10-30 21:01+0000\n"
+"PO-Revision-Date: 2019-11-02 09:41+0100\n"
 "Last-Translator: Marek Černocký <marek@manet.cz>\n"
 "Language-Team: čeština <gnome-cs-list@gnome.org>\n"
 "Language: cs\n"
@@ -56,7 +56,7 @@ msgstr ""
 "Můžete také hledat určité typy míst, jako „restaurace poblíž ulice Kobližná, "
 "Brno“ nebo „hotely poblíž Alexanderplatz, Berlín“."
 
-#: data/org.gnome.Maps.appdata.xml.in:59
+#: data/org.gnome.Maps.appdata.xml.in:92
 msgid "The GNOME Project"
 msgstr "Projekt GNOME"
 
@@ -641,25 +641,28 @@ msgstr "Přepnout viditelnost"
 msgid "Route search by GraphHopper"
 msgstr "Hledání trasy pomocí GraphHopper"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "Hledání trasy pomocí OpenTripPlanner"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results "
-"shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider "
+"could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
 msgstr ""
-"Jízdní řády pro veřejnou dopravu jsou poskytovány projektem GNOME\n"
-"díky datům získaným od přepravních společností nebo organizací.\n"
-"Tyto společnosti a organizace nenesou žádnou zodpovědnost za zobrazené\n"
-"výsledky. GNOME nemůže ručit za správnost cestovních rozvrhů a plánů.\n"
-"Kde je to možné, lze názvy a značky považovat za registrované známky."
+"Jízdní řády pro veřejnou dopravu jsou poskytovány službami třetích stran.\n"
+"GNOME nemůže nijak zaručit správnost cestovních rozvrhů a plánů.\n"
+"Může se stát, že poskytovatel nenabízí všechny dostupné způsoby dopravy, "
+"například národní\n"
+"poskytovatel nebude nabízet mezinárodní leteckou dopravu a u místního "
+"poskytovatele mohou\n"
+"scházet celostátní vlaky apod.\n"
+"Zobrazené názvy a značky považujte za registrované ochranné známky, všude "
+"tam, kde tomu tak má být."
 
 #: data/ui/social-place-more-results-row.ui:8
 msgid "Show more results"
@@ -715,6 +718,10 @@ msgstr "metro"
 msgid "Ferries"
 msgstr "trajekty"
 
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "letadla"
+
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
 msgstr "Současné místo"
@@ -757,6 +764,10 @@ msgstr "Cesta k místní struktuře složek s dlaždicemi"
 msgid "Show the version of the program"
 msgstr "Zobrazit verzi programu"
 
+#: src/application.js:104
+msgid "Ignore network availability"
+msgstr "Ignorovat dostupnost sítě"
+
 #: src/checkInDialog.js:167
 msgid "Select an account"
 msgstr "Výběr účtu"
@@ -887,15 +898,15 @@ msgstr "chyba zpracování"
 msgid "unknown geometry"
 msgstr "neznámé geometrické údaje"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:192
 msgid "Route request failed."
 msgstr "Požadavek na vyhledání cesty selhal."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:184
 msgid "No route found."
 msgstr "Nenalezena žádná cesta."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1100
 msgid "Start!"
 msgstr "Start!"
 
@@ -948,22 +959,18 @@ msgstr "Mapové dlaždice poskytuje %s"
 msgid "Search provided by %s using %s"
 msgstr "Hledání zprostředkuje %s pomocí %s"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "Typ souboru není podporován"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "Selhalo otevření vrstvy"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "Selhalo otevření adresy GeoURI"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "Pro tuto trasu nebyly nalezeny žádné časové údaje."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1377,11 +1384,16 @@ msgid "failed to load file"
 msgstr "selhalo načtení souboru"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "Odhadovaný čas: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "Plán cesty poskytl %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1414,7 +1426,7 @@ msgstr "Chůze %s"
 msgid "Arrive at %s"
 msgstr "Příjezd do %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1113
 msgid "Arrive"
 msgstr "Příjezd"
 
@@ -1446,17 +1458,25 @@ msgstr "Nebyla nalezena žádná pozdější varianta."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%-d. %B"
 
+#: src/transitPlan.js:188
+msgid "No timetable data found for this route."
+msgstr "Pro tuto trasu nebyly nalezeny žádné časové údaje."
+
+#: src/transitPlan.js:196
+msgid "No provider found for this route."
+msgstr "Pro tuto trasu nebyl nalezen žádný poskytovatel údajů."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:313
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1465,7 +1485,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:340
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1477,7 +1497,7 @@ msgstr[2] "%d minut"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:351
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1489,7 +1509,7 @@ msgstr[2] "%d hodin"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:357
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1503,7 +1523,7 @@ msgstr[2] "%d∶%02d hod"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:750
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s – %s"
@@ -1657,50 +1677,154 @@ msgid "service"
 msgstr "služba"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "neznámá"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "velmi přesná"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%f h"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%f min"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s km"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s m"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s mi"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s ft"
+
+#: src/transitplugins/openTripPlanner.js:1174
+#, javascript-format
+msgid "Continue on %s"
+msgstr "Pokračujte na %s"
+
+#: src/transitplugins/openTripPlanner.js:1176
+msgid "Continue"
+msgstr "Pokračujte"
+
+#: src/transitplugins/openTripPlanner.js:1179
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "Zahněte doleva na %s"
+
+#: src/transitplugins/openTripPlanner.js:1181
+msgid "Turn left"
+msgstr "Zahněte doleva"
+
+#: src/transitplugins/openTripPlanner.js:1184
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "Zahněte mírně doleva na %s"
+
+#: src/transitplugins/openTripPlanner.js:1186
+msgid "Turn slightly left"
+msgstr "Zahněte mírně doleva"
+
+#: src/transitplugins/openTripPlanner.js:1189
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "Zahněte ostře doleva na %s"
+
+#: src/transitplugins/openTripPlanner.js:1191
+msgid "Turn sharp left"
+msgstr "Zahněte ostře doleva"
+
+#: src/transitplugins/openTripPlanner.js:1194
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "Zahněte doprava na %s"
+
+#: src/transitplugins/openTripPlanner.js:1196
+msgid "Turn right"
+msgstr "Zahněte doprava"
+
+#: src/transitplugins/openTripPlanner.js:1199
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "Zahněte mírně doprava na %s"
+
+#: src/transitplugins/openTripPlanner.js:1201
+msgid "Turn slightly right"
+msgstr "Zahněte mírně doprava"
+
+#: src/transitplugins/openTripPlanner.js:1204
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "Zahněte ostře doprava na %s"
+
+#: src/transitplugins/openTripPlanner.js:1206
+msgid "Turn sharp right"
+msgstr "Zahněte ostře doprava"
+
+#: src/transitplugins/openTripPlanner.js:1212
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "Na kruhovém objezdu použijte výjezd %s"
+
+#: src/transitplugins/openTripPlanner.js:1214
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "Na kruhovém objezdu použijte výjezd směr %s"
+
+#: src/transitplugins/openTripPlanner.js:1216
+msgid "Take the roundabout"
+msgstr "Najeďte na kruhový objezd"
+
+#: src/transitplugins/openTripPlanner.js:1220
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "Použijte výtah a vystupte v %s"
+
+#: src/transitplugins/openTripPlanner.js:1222
+msgid "Take the elevator"
+msgstr "Použijte výtah"
+
+#: src/transitplugins/openTripPlanner.js:1226
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "Proveďte otočku doleva na %s"
+
+#: src/transitplugins/openTripPlanner.js:1228
+msgid "Make a left u-turn"
+msgstr "Proveďte otočku doleva"
+
+#: src/transitplugins/openTripPlanner.js:1231
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "Proveďte otočku doprava na %s"
+
+#: src/transitplugins/openTripPlanner.js:1233
+msgid "Make a rigth u-turn"
+msgstr "Proveďte otočku doprava"
diff --git a/po/es.po b/po/es.po
index 166f9e8..8feff5d 100644
--- a/po/es.po
+++ b/po/es.po
@@ -8,8 +8,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gnome-maps master\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-08-02 06:11+0000\n"
-"PO-Revision-Date: 2019-08-02 09:04+0200\n"
+"POT-Creation-Date: 2019-10-30 21:01+0000\n"
+"PO-Revision-Date: 2019-11-05 15:07+0100\n"
 "Last-Translator: Daniel Mustieles <daniel.mustieles@gmail.com>\n"
 "Language-Team: Spanish - Spain <gnome-es-list@gnome.org>\n"
 "Language: es_ES\n"
@@ -17,7 +17,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Gtranslator 3.32.1\n"
+"X-Generator: Gtranslator 3.34.0\n"
 
 #: data/org.gnome.Maps.appdata.xml.in:6
 msgid "GNOME Maps"
@@ -56,7 +56,7 @@ msgstr ""
 "También puede buscar por tipos de ubicaciones específicos como «Bares cerca "
 "de la Gran Vía, Madrid» u «Hoteles cerca de la Plaza Mayor, Madrid»."
 
-#: data/org.gnome.Maps.appdata.xml.in:59
+#: data/org.gnome.Maps.appdata.xml.in:92
 msgid "The GNOME Project"
 msgstr "El Proyecto GNOME"
 
@@ -69,7 +69,7 @@ msgstr "El Proyecto GNOME"
 #.
 #. Translators: This is the program name.
 #: data/org.gnome.Maps.desktop.in:4 data/ui/main-window.ui:26
-#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:509
+#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:513
 msgid "Maps"
 msgstr "Mapas"
 
@@ -103,8 +103,6 @@ msgid "Zoom level"
 msgstr "Nivel de ampliación"
 
 #: data/org.gnome.Maps.gschema.xml:21
-#| msgctxt "shortcut window"
-#| msgid "Map View"
 msgid "Map type"
 msgstr "Tipo de mapa"
 
@@ -642,28 +640,30 @@ msgstr "Conmutar estado visible"
 msgid "Route search by GraphHopper"
 msgstr "Ruta buscada por GraphHopper"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "Ruta buscada por OpenTripPlanner"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results "
-"shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider "
+"could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
 msgstr ""
-"Los itinerarios de las rutas para viajes públicos los proporciona GNOME\n"
-"usando datos de tablas de tiempos obtenidos de compañías o agencias de "
-"viajes.\n"
-"Las compañías y agencias no son responsables de los resultados mostrados.\n"
+"Los itinerarios de las rutas para viajes públicos los proporciona\n"
+"un servicio de terceros.\n"
 "GNOME no puede garantizar la corrección de los itinerarios y los tiempos "
 "mostrados.\n"
-"Los nombres y las marcas se consideran marcas registradas cuando sea "
-"aplicable."
+"Algunos proveedores pueden no incluir todos mos medios de transporte "
+"disponibles,\n"
+"ej. un proveedor nacional puede no incluir aerolíneas y un proveedor local "
+"puede\n"
+"no incluir trenes regionales.\n"
+"Los nombres y las marcas mostradas se consideran marcas registradas cuando "
+"sea aplicable."
 
 #: data/ui/social-place-more-results-row.ui:8
 msgid "Show more results"
@@ -719,6 +719,10 @@ msgstr "Metro"
 msgid "Ferries"
 msgstr "Ferris"
 
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "Aviones"
+
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
 msgstr "Ubicación actual"
@@ -761,6 +765,10 @@ msgstr "Una ruta a la estructura de carpetas locales de cuadrículas"
 msgid "Show the version of the program"
 msgstr "Mostrar la versión del programa"
 
+#: src/application.js:104
+msgid "Ignore network availability"
+msgstr "Ignorar la disponibilidad de la red"
+
 #: src/checkInDialog.js:167
 msgid "Select an account"
 msgstr "Seleccionar una cuenta"
@@ -891,15 +899,15 @@ msgstr "error al analizar"
 msgid "unknown geometry"
 msgstr "geometría desconocida"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:192
 msgid "Route request failed."
 msgstr "Falló al solicitar la ruta."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:184
 msgid "No route found."
 msgstr "No se ha encontrado la ruta."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1100
 msgid "Start!"
 msgstr "Empezar"
 
@@ -907,23 +915,23 @@ msgstr "Empezar"
 msgid "All Layer Files"
 msgstr "Todos los archivos de capas"
 
-#: src/mainWindow.js:442
+#: src/mainWindow.js:446
 msgid "Failed to connect to location service"
 msgstr "Falló al conectar al servicio de ubicación"
 
-#: src/mainWindow.js:507
+#: src/mainWindow.js:511
 msgid "translator-credits"
 msgstr "Daniel Mustieles <daniel.mustieles@gmail.com>, 2013"
 
-#: src/mainWindow.js:510
+#: src/mainWindow.js:514
 msgid "A map application for GNOME"
 msgstr "Una aplicación de mapas para GNOME"
 
-#: src/mainWindow.js:521
+#: src/mainWindow.js:525
 msgid "Copyright © 2011 – 2019 Red Hat, Inc. and The GNOME Maps authors"
 msgstr "Copyright © 2011 – 2019 Red Hat, Inc. y los autores de Mapas de GNOME"
 
-#: src/mainWindow.js:541
+#: src/mainWindow.js:545
 #, javascript-format
 msgid "Map data by %s and contributors"
 msgstr "Datos de mapas por %s y colaboradores"
@@ -933,7 +941,7 @@ msgstr "Datos de mapas por %s y colaboradores"
 #. * the bare name of the tile provider, or a linkified URL if one
 #. * is available
 #.
-#: src/mainWindow.js:557
+#: src/mainWindow.js:561
 #, javascript-format
 msgid "Map tiles provided by %s"
 msgstr "Cuadrículas de mapas proporcionadas por %s"
@@ -947,27 +955,23 @@ msgstr "Cuadrículas de mapas proporcionadas por %s"
 #. * (i.e. "%2$s ... %1$s ..." for positioning the project URL
 #. * before the provider).
 #.
-#: src/mainWindow.js:586
+#: src/mainWindow.js:590
 #, javascript-format
 msgid "Search provided by %s using %s"
 msgstr "Búsqueda proporcionada por %s usando %s"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "Tipo de archivo no soportado"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "Falló al abrir la capa"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "Falló al abrir el GeoURI"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "No hay datos en la tabla de tiempos para esta ruta."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1269,7 +1273,7 @@ msgstr "Acceso para sillas de ruedas:"
 msgid "Phone:"
 msgstr "Teléfono:"
 
-#: src/placeEntry.js:205
+#: src/placeEntry.js:209
 msgid "Failed to parse Geo URI"
 msgstr "Falló al analizar el URI de Geo"
 
@@ -1382,11 +1386,16 @@ msgid "failed to load file"
 msgstr "falló al cargar el archivo"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "Tiempo estimado: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "Itinerarios proporcionados por %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1419,7 +1428,7 @@ msgstr "Caminar %s"
 msgid "Arrive at %s"
 msgstr "Llegar a %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1113
 msgid "Arrive"
 msgstr "Llegar"
 
@@ -1451,17 +1460,25 @@ msgstr "No hay alternativas posteriores."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%b %e"
 
+#: src/transitPlan.js:188
+msgid "No timetable data found for this route."
+msgstr "No hay datos en la tabla de tiempos para esta ruta."
+
+#: src/transitPlan.js:196
+msgid "No provider found for this route."
+msgstr "No se ha encontrado un proveedor para esta ruta."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:313
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1470,7 +1487,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:340
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1481,7 +1498,7 @@ msgstr[1] "%d minutos"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:351
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1492,7 +1509,7 @@ msgstr[1] "%d horas"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:357
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1505,7 +1522,7 @@ msgstr[1] "%d:%02d horas"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:750
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s–%s"
@@ -1659,54 +1676,161 @@ msgid "service"
 msgstr "servicio"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "Desconocida"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "Exacta"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%f h"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%f min"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s km"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s m"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s mi"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s ft"
 
+#: src/transitplugins/openTripPlanner.js:1174
+#, javascript-format
+msgid "Continue on %s"
+msgstr "Continúe por %s"
+
+#: src/transitplugins/openTripPlanner.js:1176
+msgid "Continue"
+msgstr "Continúe"
+
+#: src/transitplugins/openTripPlanner.js:1179
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "Gire a la izquierda en %s"
+
+#: src/transitplugins/openTripPlanner.js:1181
+msgid "Turn left"
+msgstr "Gire a la izquierda"
+
+#: src/transitplugins/openTripPlanner.js:1184
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "Gire levemente a la izquierda en %s"
+
+#: src/transitplugins/openTripPlanner.js:1186
+msgid "Turn slightly left"
+msgstr "Gire levemente a la izquierda"
+
+#: src/transitplugins/openTripPlanner.js:1189
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "Gire bruscamente a la izquierda en %s"
+
+#: src/transitplugins/openTripPlanner.js:1191
+msgid "Turn sharp left"
+msgstr "Gire bruscamente a la izquierda"
+
+#: src/transitplugins/openTripPlanner.js:1194
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "Gire a la derecha en %s"
+
+#: src/transitplugins/openTripPlanner.js:1196
+msgid "Turn right"
+msgstr "Gire a la derecha"
+
+#: src/transitplugins/openTripPlanner.js:1199
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "Gire levemente a la derecha en %s"
+
+#: src/transitplugins/openTripPlanner.js:1201
+msgid "Turn slightly right"
+msgstr "Gire levemente a la derecha"
+
+#: src/transitplugins/openTripPlanner.js:1204
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "Gire bruscamente a la derecha en %s"
+
+#: src/transitplugins/openTripPlanner.js:1206
+msgid "Turn sharp right"
+msgstr "Gire bruscamente a la derecha"
+
+#: src/transitplugins/openTripPlanner.js:1212
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "En la rotonda, coja la salida %s"
+
+#: src/transitplugins/openTripPlanner.js:1214
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "En la rotonda, coja la salida hacia %s"
+
+#: src/transitplugins/openTripPlanner.js:1216
+msgid "Take the roundabout"
+msgstr "Entre en la rotonda"
+
+#: src/transitplugins/openTripPlanner.js:1220
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "Coja el ascensor y salga en %s"
+
+#: src/transitplugins/openTripPlanner.js:1222
+msgid "Take the elevator"
+msgstr "Coja el ascensor"
+
+#: src/transitplugins/openTripPlanner.js:1226
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "Dé la vuelta hacia la izquierda en %s"
+
+#: src/transitplugins/openTripPlanner.js:1228
+msgid "Make a left u-turn"
+msgstr "Dé la vuelta hacia la izquierda"
+
+#: src/transitplugins/openTripPlanner.js:1231
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "Dé la vuelta hacia la derecha en %s"
+
+#: src/transitplugins/openTripPlanner.js:1233
+msgid "Make a rigth u-turn"
+msgstr "Dé la vuelta hacia la derecha"
+
+#~ msgid "Route search by OpenTripPlanner"
+#~ msgstr "Ruta buscada por OpenTripPlanner"
+
 #~ msgid "Open with another application"
 #~ msgstr "Abrir con otra aplicación"
 
diff --git a/po/eu.po b/po/eu.po
index ea648ec..ed592e6 100644
--- a/po/eu.po
+++ b/po/eu.po
@@ -9,8 +9,8 @@
 msgid ""
 msgstr "Project-Id-Version: gnome-maps master\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-08-02 06:11+0000\n"
-"PO-Revision-Date: 2019-08-11 10:00+0100\n"
+"POT-Creation-Date: 2019-10-13 19:50+0000\n"
+"PO-Revision-Date: 2019-10-20 10:00+0100\n"
 "Last-Translator: Asier Sarasua Garmendia <asier.sarasua@gmail.com>\n"
 "Language-Team: Basque <librezale@librezale.eus>\n"
 "Language: eu\n"
@@ -21,7 +21,7 @@ msgstr "Project-Id-Version: gnome-maps master\n"
 
 #: data/org.gnome.Maps.appdata.xml.in:6
 msgid "GNOME Maps"
-msgstr "GNOMEren Mapak"
+msgstr "GNOME Mapak"
 
 #: data/org.gnome.Maps.appdata.xml.in:7
 msgid "Find places around the world"
@@ -49,7 +49,7 @@ msgid ""
 "Street, Boston” or “Hotels near Alexanderplatz, Berlin”."
 msgstr "Bilatu kokaleku mota zehatz bat, adibidez 'Diru-truke bulegoa, Bilbo' edo 'Hotelak kale nagusia, Bilbo'"
 
-#: data/org.gnome.Maps.appdata.xml.in:59
+#: data/org.gnome.Maps.appdata.xml.in:92
 msgid "The GNOME Project"
 msgstr "GNOME proiektua"
 
@@ -62,7 +62,7 @@ msgstr "GNOME proiektua"
 #.
 #. Translators: This is the program name.
 #: data/org.gnome.Maps.desktop.in:4 data/ui/main-window.ui:26
-#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:509
+#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:513
 msgid "Maps"
 msgstr "Mapak"
 
@@ -101,7 +101,7 @@ msgstr "Mapa mota"
 
 #: data/org.gnome.Maps.gschema.xml:22
 msgid "The type of map to display (street, aerial, etc.)"
-msgstr "Bistaratuko den mapa mota (kalekoa, airekoa, etab.)"
+msgstr "Bistaratuko den mapa mota (kalekoa, airekoa eta abar)"
 
 #: data/org.gnome.Maps.gschema.xml:26
 msgid "Window size"
@@ -468,7 +468,7 @@ msgstr "Lagundu mapa hobetzen\n"
 
 #: data/ui/osm-account-dialog.ui:56
 msgid "Email"
-msgstr "Helb. el."
+msgstr "Posta elektronikoa"
 
 #: data/ui/osm-account-dialog.ui:81
 msgid "Password"
@@ -616,23 +616,21 @@ msgstr "Txandakatu ikusgaitasuna"
 msgid "Route search by GraphHopper"
 msgstr "ibilbidea GraphHopper-ek bilatuta"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "ibilbidea OpenTripPlanner-ek bilatuta"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results "
-"shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider "
+"could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
-msgstr "Ibilbide publikoen bideak GNOMEk eskaintzen ditu,\n"
-"konpainia eta agentzien zirkulazioetatik lortutako ordutegiaren datuak erabiliz.\n"
-"Konpainiak eta agentziak ez dira erakutsitako emaitzen arduradun izango.\n"
-"GNOMEk ezin du erakutsitako ibilbideen eta antolamenduen zuzentasunaz ziurtatu.\n"
+msgstr "Ibilbide publikoen bideak hirugarrenek eskaintzen dituzte.\n"
+"GNOMEk ezin du bermatu erakutsitako ibilbideen eta ordutegien zuzentasuna.\n"
+"Kontuan izan zenbait hornitzailek ez dituztela garraio modu guztiak kontuan hartzen, adibidez nazio mailako hornitzaile batek beharbada ez ditu hegaldiak kontuan hartzen, eta hornitzaile lokal batek agian ez ditu eskualde mailako trenak erakusten.\n"
 "Erakutsitako izenak eta markak erregistratutako marka gisa hartuko dira dagokienenean."
 
 #: data/ui/social-place-more-results-row.ui:8
@@ -687,7 +685,11 @@ msgstr "Metroak"
 
 #: data/ui/transit-options-panel.ui:145
 msgid "Ferries"
-msgstr "Ferriak"
+msgstr "Ferryak"
+
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "Hegazkinak"
 
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
@@ -777,7 +779,7 @@ msgstr "Errore bat gertatu da"
 #: src/checkIn.js:144
 #, javascript-format
 msgid "Cannot find “%s” in the social service"
-msgstr "Ezin da “%s“ aurkitu zerbitzu sozialean"
+msgstr "Ezin da “%s” aurkitu zerbitzu sozialean"
 
 #: src/checkIn.js:146
 msgid "Cannot find a suitable place to check-in in this location"
@@ -852,15 +854,15 @@ msgstr "analisi-errorea"
 msgid "unknown geometry"
 msgstr "geometria ezezaguna"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:169
 msgid "Route request failed."
 msgstr "Huts egin du ibilbidearen eskaerak."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:161
 msgid "No route found."
 msgstr "Ez da ibilbiderik aurkitu."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1164
 msgid "Start!"
 msgstr "Hasi!"
 
@@ -868,23 +870,23 @@ msgstr "Hasi!"
 msgid "All Layer Files"
 msgstr "Geruzen fitxategi guztiak"
 
-#: src/mainWindow.js:442
+#: src/mainWindow.js:446
 msgid "Failed to connect to location service"
 msgstr "Huts egin du kokapen-zerbitzuarekin konektatzean"
 
-#: src/mainWindow.js:507
+#: src/mainWindow.js:511
 msgid "translator-credits"
 msgstr "Asier Sarasua Garmendia <asier.sarasua@gmail.com>"
 
-#: src/mainWindow.js:510
+#: src/mainWindow.js:514
 msgid "A map application for GNOME"
 msgstr "GNOMEren mapen aplikazioa"
 
-#: src/mainWindow.js:521
+#: src/mainWindow.js:525
 msgid "Copyright © 2011 – 2019 Red Hat, Inc. and The GNOME Maps authors"
 msgstr "Copyright © 2011 – 2019 Red Hat, Inc. eta GNOME Maps egileak"
 
-#: src/mainWindow.js:541
+#: src/mainWindow.js:545
 #, javascript-format
 msgid "Map data by %s and contributors"
 msgstr "Maparen datuen sortzaileak: %s eta laguntzaileak"
@@ -894,7 +896,7 @@ msgstr "Maparen datuen sortzaileak: %s eta laguntzaileak"
 #. * the bare name of the tile provider, or a linkified URL if one
 #. * is available
 #.
-#: src/mainWindow.js:557
+#: src/mainWindow.js:561
 #, javascript-format
 msgid "Map tiles provided by %s"
 msgstr "Mapa-lauzen hornitzaileak: %s"
@@ -908,27 +910,23 @@ msgstr "Mapa-lauzen hornitzaileak: %s"
 #. * (i.e. "%2$s ... %1$s ..." for positioning the project URL
 #. * before the provider).
 #.
-#: src/mainWindow.js:586
+#: src/mainWindow.js:590
 #, javascript-format
 msgid "Search provided by %s using %s"
 msgstr "%s motorrak hornitutako bilaketa %s bidez"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "Fitxategi mota ez dago onartuta"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "Huts egin du geruza irekitzean"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "Huts egin du GeoURIa irekitzean"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "Ez da ordutegiaren daturik aurkitu ibilbide honetan."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1220,7 +1218,7 @@ msgstr "Gurpil-aulkien irisgarritasuna:"
 msgid "Phone:"
 msgstr "Telefonoa:"
 
-#: src/placeEntry.js:205
+#: src/placeEntry.js:209
 msgid "Failed to parse Geo URI"
 msgstr "Huts egin du Geo URIa aztertzean"
 
@@ -1283,7 +1281,7 @@ msgstr "URLa ez da onartzen"
 #: src/printLayout.js:312
 #, javascript-format
 msgid "From %s to %s"
-msgstr "\"%s\"(e)ndik \"%s\"(e)ra"
+msgstr "“%s”(e)ndik “%s”(e)ra"
 
 #: src/printOperation.js:46
 msgid "Loading map tiles for printing"
@@ -1333,11 +1331,16 @@ msgid "failed to load file"
 msgstr "huts egin du fitxategia kargatzean"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "Aurreikusitako denbora: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "Ibilbideen hornitzailea: %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1370,7 +1373,7 @@ msgstr "Oinez: %s"
 msgid "Arrive at %s"
 msgstr "Iritsi: %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1177
 msgid "Arrive"
 msgstr "Iritsi"
 
@@ -1402,17 +1405,25 @@ msgstr "Ez da ondorengoko ordezkorik aurkitu."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%b %e"
 
+#: src/transitPlan.js:165
+msgid "No timetable data found for this route."
+msgstr "Ez da ordutegiaren daturik aurkitu ibilbide honetan."
+
+#: src/transitPlan.js:173
+msgid "No provider found for this route."
+msgstr "Ez da hornitzailerik aurkitu ibilbide honetarako."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:290
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1421,7 +1432,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:317
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1432,7 +1443,7 @@ msgstr[1] "%d minutu"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:328
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1443,7 +1454,7 @@ msgstr[1] "%d ordu"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:334
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1456,7 +1467,7 @@ msgstr[1] "%d:%0d ordu"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:699
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s–%s"
@@ -1610,54 +1621,161 @@ msgid "service"
 msgstr "zerbitzua"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "Ezezaguna"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "Zehatza"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%f o"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%f min"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s km"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s m"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s mi"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s ft"
 
+#: src/transitplugins/openTripPlanner.js:1238
+#, javascript-format
+msgid "Continue on %s"
+msgstr "Jarraitu hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1240
+msgid "Continue"
+msgstr "Jarraitu"
+
+#: src/transitplugins/openTripPlanner.js:1243
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "Biratu ezkerrera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1245
+msgid "Turn left"
+msgstr "Biratu ezkerrera"
+
+#: src/transitplugins/openTripPlanner.js:1248
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "Biratu pixka bat ezkerrera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1250
+msgid "Turn slightly left"
+msgstr "Biratu pixka bat ezkerrera"
+
+#: src/transitplugins/openTripPlanner.js:1253
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "Biratu erabat ezkerrera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1255
+msgid "Turn sharp left"
+msgstr "Biratu erabat ezkerrera"
+
+#: src/transitplugins/openTripPlanner.js:1258
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "Biratu eskuinera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1260
+msgid "Turn right"
+msgstr "Biratu eskuinera"
+
+#: src/transitplugins/openTripPlanner.js:1263
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "Biratu pixka bat eskuinera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1265
+msgid "Turn slightly right"
+msgstr "Biratu pixka bat eskuinera"
+
+#: src/transitplugins/openTripPlanner.js:1268
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "Biratu erabat eskuinera hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1270
+msgid "Turn sharp right"
+msgstr "Biratu erabat eskuinera"
+
+#: src/transitplugins/openTripPlanner.js:1276
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "Biribilgunean, hartu %s irteera"
+
+#: src/transitplugins/openTripPlanner.js:1278
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "Biribilgunean, hartu %s helbidera joateko irteera"
+
+#: src/transitplugins/openTripPlanner.js:1280
+msgid "Take the roundabout"
+msgstr "Hartu biribilgunea"
+
+#: src/transitplugins/openTripPlanner.js:1284
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "Hartu igogailua eta utzi hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1286
+msgid "Take the elevator"
+msgstr "Hartu igogailua"
+
+#: src/transitplugins/openTripPlanner.js:1290
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "Aldatu noranzkoa ezkerrerantz hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1292
+msgid "Make a left u-turn"
+msgstr "Aldatu noranzkoa ezkerrerantz"
+
+#: src/transitplugins/openTripPlanner.js:1295
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "Aldatu noranzkoa eskuinerantz hemen: %s"
+
+#: src/transitplugins/openTripPlanner.js:1297
+msgid "Make a rigth u-turn"
+msgstr "Aldatu noranzkoa eskuinerantz"
+
+#~ msgid "Route search by OpenTripPlanner"
+#~ msgstr "ibilbidea OpenTripPlanner-ek bilatuta"
+
 #~ msgid "org.gnome.Maps"
 #~ msgstr "org.gnome.Maps"
 
diff --git a/po/fa.po b/po/fa.po
index 005a8cb..590cfad 100644
--- a/po/fa.po
+++ b/po/fa.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gnome-maps gnome-3-10\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-09-22 06:02+0000\n"
-"PO-Revision-Date: 2019-09-25 12:05+0000\n"
+"POT-Creation-Date: 2019-10-19 13:44+0000\n"
+"PO-Revision-Date: 2019-10-19 16:36+0000\n"
 "Last-Translator: Danial Behzadi <dani.behzi@ubuntu.com>\n"
 "Language-Team: Persian\n"
 "Language: fa\n"
@@ -16,7 +16,7 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Poedit 2.2.1\n"
+"X-Generator: Poedit 2.2.4\n"
 
 #: data/org.gnome.Maps.appdata.xml.in:6
 msgid "GNOME Maps"
@@ -29,20 +29,20 @@ msgstr "یافتن مکان‌ها در هر جایی از جهان"
 #: data/org.gnome.Maps.appdata.xml.in:9
 msgid ""
 "Maps gives you quick access to maps all across the world. It allows you to "
-"quickly find the place you’re looking for by searching for a city or street, "
-"or locate a place to meet a friend."
+"quickly find the place you’re looking for by searching for a city or street, or "
+"locate a place to meet a friend."
 msgstr ""
-"نقشه‌ها دسترسی سریعی به نقشه‌ها در تمام جهان را می‌دهد. این برنامه می‌گذارد با "
-"جست‌وجو برای یک شهر یا خیابان، مکانی را که به دنبالشید یافته یا مکانی را برای "
-"قرار مشخّص کنید."
+"نقشه‌ها دسترسی سریعی به نقشه‌ها در تمام جهان را می‌دهد. این برنامه می‌گذارد با جست‌وجو "
+"برای یک شهر یا خیابان، مکانی را که به دنبالشید یافته یا مکانی را برای قرار مشخّص "
+"کنید."
 
 #: data/org.gnome.Maps.appdata.xml.in:14
 msgid ""
-"Maps uses the collaborative OpenStreetMap database, made by hundreds of "
-"thousands of people across the globe."
+"Maps uses the collaborative OpenStreetMap database, made by hundreds of thousands "
+"of people across the globe."
 msgstr ""
-"نقشه‌ها از پایگاه دادهٔ مشارکتی OpenStreetMap استفاده می‌کند که به دست صدها هزار "
-"نفر در سراسر جهان درست شده است."
+"نقشه‌ها از پایگاه دادهٔ مشارکتی OpenStreetMap استفاده می‌کند که به دست صدها هزار نفر "
+"در سراسر جهان درست شده است."
 
 #. Translators: Search is carried out on OpenStreetMap data using Nominatim.
 #. Visit http://wiki.openstreetmap.org/wiki/Nominatim/Special_Phrases and click
@@ -52,8 +52,8 @@ msgid ""
 "You can even search for specific types of locations, such as “Pubs near Main "
 "Street, Boston” or “Hotels near Alexanderplatz, Berlin”."
 msgstr ""
-"حتا می‌توانید گونه‌های خاصی از موقعیت‌ها را، مثل «Pubs near Main Street, Boston» "
-"یا «WiFi Access near Alexanderplatz, Berlin» جست‌وجو کنید."
+"حتا می‌توانید گونه‌های خاصی از موقعیت‌ها را، مثل «Pubs near Main Street, Boston» یا "
+"«WiFi Access near Alexanderplatz, Berlin» جست‌وجو کنید."
 
 #: data/org.gnome.Maps.appdata.xml.in:92
 msgid "The GNOME Project"
@@ -67,8 +67,8 @@ msgstr "پروژهٔ گنوم"
 #. * overview.
 #.
 #. Translators: This is the program name.
-#: data/org.gnome.Maps.desktop.in:4 data/ui/main-window.ui:26
-#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:513
+#: data/org.gnome.Maps.desktop.in:4 data/ui/main-window.ui:26 src/application.js:81
+#: src/mainWindow.js:139 src/mainWindow.js:513
 msgid "Maps"
 msgstr "نقشه‌ها"
 
@@ -179,8 +179,8 @@ msgid ""
 "Latest used Foursquare check-in privacy setting. Possible values are: public, "
 "followers or private."
 msgstr ""
-"آخرین تنظیمات استفاده شدهٔ محرمانگی اعلام حضور فوراسکور. مقادیر مجاز عبارتند "
-"از: public، followers یا private."
+"آخرین تنظیمات استفاده شدهٔ محرمانگی اعلام حضور فوراسکور. مقادیر مجاز عبارتند از: "
+"public، followers یا private."
 
 #: data/org.gnome.Maps.gschema.xml:67
 msgid "Foursquare check-in Facebook broadcasting"
@@ -188,8 +188,8 @@ msgstr "اعلام حضور در فوراسکور و انتشار در فیس‌
 
 #: data/org.gnome.Maps.gschema.xml:68
 msgid ""
-"Indicates if Foursquare should broadcast the check-in as a post in the "
-"Facebook account associated with the Foursquare account."
+"Indicates if Foursquare should broadcast the check-in as a post in the Facebook "
+"account associated with the Foursquare account."
 msgstr ""
 "مشخّص می‌کند که آیا فوراسکور باید اعلام حضور را به عنوان یک فرسته در حساب فیس‌بوک "
 "مرتبط با حساب فوراسکور منتشر کند یا نه."
@@ -200,8 +200,8 @@ msgstr "اعلام حضور فوراسکور و انتشار در توییتر"
 
 #: data/org.gnome.Maps.gschema.xml:73
 msgid ""
-"Indicates if Foursquare should broadcast the check-in as a tweet in the "
-"Twitter account associated with the Foursquare account."
+"Indicates if Foursquare should broadcast the check-in as a tweet in the Twitter "
+"account associated with the Foursquare account."
 msgstr ""
 "مشخّص می‌کند که آیا فوراسکور باید اعلام حضور را به عنوان یک توییت در حساب توییتر "
 "مرتبط با حساب فوراسکور منتشر کند یا نه."
@@ -212,8 +212,7 @@ msgstr "نام‌کاربری یا نشانی رایانامهٔ OpenStreetMap"
 
 #: data/org.gnome.Maps.gschema.xml:78
 msgid "Indicates if the user has signed in to edit OpenStreetMap data."
-msgstr ""
-"مشخّص می‌کند که آیا کاربر برای ویرایش داده‌های OpenStreetMap وارد شده یا نه."
+msgstr "مشخّص می‌کند که آیا کاربر برای ویرایش داده‌های OpenStreetMap وارد شده یا نه."
 
 #: data/org.gnome.Maps.gschema.xml:82
 msgid "Last used transportation type for routing"
@@ -502,8 +501,8 @@ msgstr "حسابی ندارید؟"
 #: data/ui/osm-account-dialog.ui:159
 msgid ""
 "Sorry, that didn’t work. Please try again, or visit\n"
-"<a href=\"https://www.openstreetmap.org/user/forgot-password\">OpenStreetMap</"
-"a> to reset your password."
+"<a href=\"https://www.openstreetmap.org/user/forgot-password\">OpenStreetMap</a> "
+"to reset your password."
 msgstr ""
 "متأسّفانه تأثیری نداشت. لطفا دوباره تلاش کرده یا برای\n"
 "باز نشانی گذرواژه‌تان به <a href=\"https://www.openstreetmap.org/user/forgot-"
@@ -572,7 +571,7 @@ msgid ""
 "OpenStreetMap data."
 msgstr ""
 "تغییرات نقشه در تمام نقشه‌هایی که از داده‌های\n"
-"    OpenStreetMap استفاده می‌کنند قابل مشاهده است."
+"اوپن‌استریت‌مپ استفاده می‌کنند قابل مشاهده است."
 
 #: data/ui/osm-edit-dialog.ui:241
 msgid "Recently Used"
@@ -635,25 +634,25 @@ msgstr "تغییر وضعیت نمایانی"
 msgid "Route search by GraphHopper"
 msgstr "جست‌وجوی مسیر به دست GraphHopper"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "جست‌وجوی مسیر به دست OpenTripPlanner"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
 msgstr ""
-"مسیریابی برنامهٔ سفر برای حمل و نقل عمومی با استفاده از داده‌های جدول زمانی\n"
-"گرفته شده از آژانس‌ها یا شرکت‌های مسافرتی به دست گنوم فراهم شده است.\n"
-"آژانس‌ها و شرکت‌ها نمی‌توانند در قبال نتایج نمایش‌داده شده مسيول باشند.\n"
-"گنوم نمی‌تواند درستی رمان‌بندی برنامه‌های سفر نمایش‌داده شده را تضمین کند.\n"
-"نام‌ها و مارک‌های نمایش‌داده شده، در صورت امکان باید علامت‌های تجاری ثبت‌شده در نظر "
-"گرفته شوند."
+"مسیریابی برنامهٔ سفر برای حمل‌ونقل عمومی به دست خدمات ثالث فراهم شده است.\n"
+"گنوم نمی‌تواند درستی برنامهٔ سفر و زمان‌بندی‌های نمایش داده‌شده را تصمین کند.\n"
+"به خاطر داشته باشید که ممکن است برخی فراهم‌کنندگان، تمام حالت‌های موجود برای\n"
+"حمل‌ونقل را نداشته باشند. مثلاً ممکن است یک فراهم‌کنندهٔ ملّی، خطوط هوایی را نداشته\n"
+"باشد و یک فراهم‌کنندهٔ محلّی می‌تواند قطارهای منطقه‌ای را نشان ندهد.\n"
+"نام‌ها و برندهای نمایش‌داده‌شده باید هنگام امکان به عنوان علایم تجاری در نظر گرفته "
+"شوند."
 
 #: data/ui/social-place-more-results-row.ui:8
 msgid "Show more results"
@@ -709,6 +708,10 @@ msgstr "مترو"
 msgid "Ferries"
 msgstr "قایق‌ها"
 
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "هواپیماها"
+
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
 msgstr "موقعیت کنونی"
@@ -765,16 +768,16 @@ msgstr "گزینش یک موقعیت"
 
 #: src/checkInDialog.js:201
 msgid ""
-"Maps cannot find the place to check in to with Facebook. Please select one "
-"from this list."
+"Maps cannot find the place to check in to with Facebook. Please select one from "
+"this list."
 msgstr ""
-"نقشه‌ها نمی‌تواند مکان را برای اعلام حضور در فیس‌بوک بیابد. لطفاً موقعیتی را از "
-"فهرست برگزینید."
+"نقشه‌ها نمی‌تواند مکان را برای اعلام حضور در فیس‌بوک بیابد. لطفاً موقعیتی را از فهرست "
+"برگزینید."
 
 #: src/checkInDialog.js:203
 msgid ""
-"Maps cannot find the place to check in to with Foursquare. Please select one "
-"from this list."
+"Maps cannot find the place to check in to with Foursquare. Please select one from "
+"this list."
 msgstr ""
 "نقشه‌ها نمی‌تواند مکان را برای اعلام حضور در فوراسکور بیابد. لطفاً موقعیتی را از "
 "فهرست برگزینید."
@@ -809,11 +812,11 @@ msgstr "نمی‌توان مکانی مناسب برای اعلام حضور د
 
 #: src/checkIn.js:150
 msgid ""
-"Credentials have expired, please open Online Accounts to sign in and enable "
-"this account"
+"Credentials have expired, please open Online Accounts to sign in and enable this "
+"account"
 msgstr ""
-"اعتبارنامه‌ها منقضی شده‌اند. لطفاً برای ورود و به کار انداختن این حساب، حساب‌های "
-"برخط را بگشایید"
+"اعتبارنامه‌ها منقضی شده‌اند. لطفاً برای ورود و به کار انداختن این حساب، حساب‌های برخط "
+"را بگشایید"
 
 #: src/contextMenu.js:99
 msgid "Route from here"
@@ -833,11 +836,11 @@ msgstr "این‌جا چیزی پیدا نشد!"
 
 #: src/contextMenu.js:190
 msgid ""
-"Location was added to the map, note that it may take a while before it shows "
-"on the map and in search results."
+"Location was added to the map, note that it may take a while before it shows on "
+"the map and in search results."
 msgstr ""
-"موقعیت به نقشه اضافه شد. خواستان باشد که نمایشش در نقشه و نتایج جست‌وجو، ممکن "
-"است کمی طول بکشد."
+"موقعیت به نقشه اضافه شد. خواستان باشد که نمایشش در نقشه و نتایج جست‌وجو، ممکن است "
+"کمی طول بکشد."
 
 #. Translators: This is a format string for a PNG filename for an
 #. * exported image with coordinates. The .png extension should be kept
@@ -880,15 +883,15 @@ msgstr "خطای تجزیه"
 msgid "unknown geometry"
 msgstr "هندسهٔ ناشناخته"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:192
 msgid "Route request failed."
 msgstr "درخواست مسیر شکست خورد."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:184
 msgid "No route found."
 msgstr "هیچ مسیری پیدا نشد."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1100
 msgid "Start!"
 msgstr "شروع!"
 
@@ -943,22 +946,18 @@ msgstr "کاشی‌های نقشه به دست %s فراهم شده"
 msgid "Search provided by %s using %s"
 msgstr "جست‌وجو به دست %s با استفاده از %s فراهم شده"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "گونهٔ پرونده پشتیبانی نمی‌شود"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "شکست در گشودن لایه"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "شکست در گشودن نشانی جغرافیایی"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "هیچ دادهٔٔ زمانی‌ای برای این مسیر پیدا نشد."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1007,8 +1006,8 @@ msgstr "پایگاه وب"
 
 #: src/osmEditDialog.js:122
 msgid ""
-"The official website. Try to use the most basic form of a URL i.e. http://"
-"example.com instead of http://example.com/index.html."
+"The official website. Try to use the most basic form of a URL i.e. http://example."
+"com instead of http://example.com/index.html."
 msgstr ""
 "پایگاه وب رسمی. سعی کنید از ساده‌ترین شکل آدرس استفاده کنید مثلا http://example."
 "com به جای http://example.com/index.html."
@@ -1031,11 +1030,10 @@ msgstr "ویکی‌پدیا"
 
 #: src/osmEditDialog.js:140
 msgid ""
-"The format used should include the language code and the article title like "
-"“en:Article title”."
+"The format used should include the language code and the article title like “en:"
+"Article title”."
 msgstr ""
-"قالب استفاده شده باید شامل کد زبان و عنوان مقاله باید مثل «en:Article title» "
-"باشد."
+"قالب استفاده شده باید شامل کد زبان و عنوان مقاله باید مثل «en:Article title» باشد."
 
 #: src/osmEditDialog.js:144
 msgid "Opening hours"
@@ -1202,12 +1200,11 @@ msgstr "نکته"
 
 #: src/osmEditDialog.js:220
 msgid ""
-"Information used to inform other mappers about non-obvious information about "
-"an element, the author’s intent when creating it, or hints for further "
-"improvement."
+"Information used to inform other mappers about non-obvious information about an "
+"element, the author’s intent when creating it, or hints for further improvement."
 msgstr ""
-"اطّلاعات برای آگاه کردن دیگر نقشه‌کش‌ها دربارهٔ اطّلاعات نابدیهی دربارهٔ یک عنصر، "
-"قصد نگارنده هنگام ایجادش یا راهنمایی برای بهبودهای بعدی."
+"اطّلاعات برای آگاه کردن دیگر نقشه‌کش‌ها دربارهٔ اطّلاعات نابدیهی دربارهٔ یک عنصر، قصد "
+"نگارنده هنگام ایجادش یا راهنمایی برای بهبودهای بعدی."
 
 #: src/osmEditDialog.js:325
 msgctxt "dialog title"
@@ -1304,7 +1301,7 @@ msgstr "تعیین شده"
 
 #: src/place.js:475
 msgid "Place not found in OpenStreetMap"
-msgstr "مکان در OpenStreetMap پیدا نشد"
+msgstr "مکان در اوپن استریت مپ پیدا نشد"
 
 #: src/place.js:495
 msgid "OpenStreetMap URL is not valid"
@@ -1371,11 +1368,16 @@ msgid "failed to load file"
 msgstr "شکست در بارگیری پرونده"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "زمان تخمینی: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "برنامه سفر فراهم شده به دست %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1408,7 +1410,7 @@ msgstr "%s قدم بزنید"
 msgid "Arrive at %s"
 msgstr "رسیدن به مقصد در %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1113
 msgid "Arrive"
 msgstr "رسیدن"
 
@@ -1440,17 +1442,25 @@ msgstr "جایگزین‌های جدیدتری پیدا نشدند."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%b %Oe"
 
+#: src/transitPlan.js:188
+msgid "No timetable data found for this route."
+msgstr "برای این مسیر هیچ دادهٔ جدول زمانی‌ای پیدا نشد."
+
+#: src/transitPlan.js:196
+msgid "No provider found for this route."
+msgstr "برای این مسیر هیچ فراهم کننده‌ای پیدا نشد."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:313
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1459,7 +1469,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:340
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1469,7 +1479,7 @@ msgstr[0] "%Id دقیقه"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:351
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1479,7 +1489,7 @@ msgstr[0] "%Id ساعت"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:357
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1491,7 +1501,7 @@ msgstr[0] "%Id:%I02d ساعت"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:734
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s–%s"
@@ -1645,54 +1655,161 @@ msgid "service"
 msgstr "خدمت"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "ناشناخته"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "دقیقاً"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%If ساعت"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%If دقیقه"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s ک‌م"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s م"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s ما"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s پا"
 
+#: src/transitplugins/openTripPlanner.js:1174
+#, javascript-format
+msgid "Continue on %s"
+msgstr "در %s ادامه دهید"
+
+#: src/transitplugins/openTripPlanner.js:1176
+msgid "Continue"
+msgstr "ادامه"
+
+#: src/transitplugins/openTripPlanner.js:1179
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "در %s به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1181
+msgid "Turn left"
+msgstr "به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1184
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "در %s آرام به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1186
+msgid "Turn slightly left"
+msgstr "آرام به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1189
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "در %s تند به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1191
+msgid "Turn sharp left"
+msgstr "تند به چپ بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1194
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "در %s به راست بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1196
+msgid "Turn right"
+msgstr "به راست بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1199
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "در %s آرام به راست بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1201
+msgid "Turn slightly right"
+msgstr "آرام به راست بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1204
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "در %s تند به راست بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1206
+msgid "Turn sharp right"
+msgstr "تند به رایت بپیچید"
+
+#: src/transitplugins/openTripPlanner.js:1212
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "در میدان از %s خارج شوید"
+
+#: src/transitplugins/openTripPlanner.js:1214
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "در میدان به %s خارج شوید"
+
+#: src/transitplugins/openTripPlanner.js:1216
+msgid "Take the roundabout"
+msgstr "وارد میدان شوید"
+
+#: src/transitplugins/openTripPlanner.js:1220
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "وارد آسانسور شده و در %s حارج شوید"
+
+#: src/transitplugins/openTripPlanner.js:1222
+msgid "Take the elevator"
+msgstr "وارد آسانسور شوید"
+
+#: src/transitplugins/openTripPlanner.js:1226
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "به %s گردش به چپ کنید"
+
+#: src/transitplugins/openTripPlanner.js:1228
+msgid "Make a left u-turn"
+msgstr "گردش به چپ کنید"
+
+#: src/transitplugins/openTripPlanner.js:1231
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "به %s گردش به راست کنید"
+
+#: src/transitplugins/openTripPlanner.js:1233
+msgid "Make a rigth u-turn"
+msgstr "گردش به راست کنید"
+
+#~ msgid "Route search by OpenTripPlanner"
+#~ msgstr "جست‌وجوی مسیر به دست OpenTripPlanner"
+
 #~ msgid "Quit"
 #~ msgstr "خروج"
 
diff --git a/po/fur.po b/po/fur.po
index 3fd8a1e..cd97bb9 100644
--- a/po/fur.po
+++ b/po/fur.po
@@ -7,15 +7,15 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gnome-maps master\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-09-10 19:50+0000\n"
-"PO-Revision-Date: 2019-09-22 07:59+0200\n"
+"POT-Creation-Date: 2019-10-01 20:20+0000\n"
+"PO-Revision-Date: 2019-10-17 21:32+0200\n"
 "Last-Translator: Fabio Tomat <f.t.public@gmail.com>\n"
 "Language-Team: Friulian <fur@li.org>\n"
 "Language: fur\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.2.3\n"
+"X-Generator: Poedit 2.2.4\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 #: data/org.gnome.Maps.appdata.xml.in:6
@@ -139,7 +139,7 @@ msgstr "Massim numar di risultâts di ricercje"
 
 #: data/org.gnome.Maps.gschema.xml:42
 msgid "Maximum number of search results from geocode search."
-msgstr "Massim numar di risultâts de ricercje da codifiche gjeografiche. "
+msgstr "Massim numar di risultâts de ricercje da codifiche gjeografiche."
 
 #: data/org.gnome.Maps.gschema.xml:46
 msgid "Number of recent places to store"
@@ -640,25 +640,27 @@ msgstr "Cambiâ stât visibil"
 msgid "Route search by GraphHopper"
 msgstr "Ricercje dal percors cun GraphHopper"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "Ricercje dal percors cun OpenTripPlanner"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results "
-"shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider "
+"could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
 msgstr ""
-"I itineraris di percors pai mieçs publics a son furnîts di GNOME\n"
-"doprant i dâts dai oraris otignûts des aziendis di traspuart o agjenziis.\n"
-"Lis aziendis di traspuart e lis agjenziis no puedin jessi tignudis "
-"responsabilis pai risultâts mostrâts.\n"
-"GNOME nol pues garantî la justece dai itineraris e dai plans mostrâts.\n"
+"I itineraris di percors pai mieçs publics a son furnîts di servizis di\n"
+"tiercis parts.\n"
+"GNOME nol pues garantî la justece dai itineraris e dai oraris mostrâts.\n"
+"Viôt che cualchi furnidôr al podarès no includi dutis lis modalitâts di "
+"traspuart disponibilis,\n"
+"p.e. un furnidôr nazionâl al podarès no includi lis tratis aeronautichis e "
+"un furnidôr locâl\n"
+"al pues no includi i trens regjionâi.\n"
 "I nons e lis marchis mostradis si àn di considerâ come marchis regjistradis "
 "cuant che si puedin aplicâ."
 
@@ -716,6 +718,10 @@ msgstr "Metropolitane"
 msgid "Ferries"
 msgstr "Traghets"
 
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "Avions"
+
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
 msgstr "Posizion atuâl"
@@ -885,17 +891,17 @@ msgstr "erôr tal analizâ"
 
 #: src/geoJSONSource.js:180
 msgid "unknown geometry"
-msgstr "Gjeometrie no valide"
+msgstr "gjeometrie no valide"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:169
 msgid "Route request failed."
 msgstr "Richieste dal percors falide."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:161
 msgid "No route found."
 msgstr "Percors no cjatât."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1154
 msgid "Start!"
 msgstr "Invie!"
 
@@ -909,7 +915,7 @@ msgstr "Impussibil conetisi al servizi di localizazion"
 
 #: src/mainWindow.js:511
 msgid "translator-credits"
-msgstr "Fabio Tomat"
+msgstr "Fabio Tomat, <f.t.public@gmail.com>"
 
 #: src/mainWindow.js:514
 msgid "A map application for GNOME"
@@ -948,22 +954,18 @@ msgstr "Tassei mape furnîts di %s"
 msgid "Search provided by %s using %s"
 msgstr "Ricercje furnide di %s doprant %s"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "Gjenar di file no supuartât"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "Impussibil vierzi il strât"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "Impussibil vierzi il GeoURI"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "Nissun dât di oraris cjatât par chest percors."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1378,11 +1380,16 @@ msgid "failed to load file"
 msgstr "impussibil cjariâ il file"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "Timp stimât: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "Itineraris furnîts di %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1415,7 +1422,7 @@ msgstr "Cjaminâ %s"
 msgid "Arrive at %s"
 msgstr "Rive a %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1167
 msgid "Arrive"
 msgstr "Rivade"
 
@@ -1447,17 +1454,25 @@ msgstr "Nissune alternative plui tart cjatade."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%e di %b"
 
+#: src/transitPlan.js:165
+msgid "No timetable data found for this route."
+msgstr "Nissun dât di oraris cjatât par chest percors."
+
+#: src/transitPlan.js:173
+msgid "No provider found for this route."
+msgstr "Nissun furnidôr cjatât par chest percors."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:290
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1466,7 +1481,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:317
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1477,7 +1492,7 @@ msgstr[1] "%d minûts"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:328
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1488,7 +1503,7 @@ msgstr[1] "%d oris"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:334
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1501,7 +1516,7 @@ msgstr[1] "%d:%02d oris"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:699
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s–%s"
@@ -1512,7 +1527,7 @@ msgstr "dut il dì"
 
 #: src/translations.js:58
 msgid "from sunrise to sunset"
-msgstr "Di un scûr a chel altri"
+msgstr "di un scûr a chel altri"
 
 #. Translators:
 #. * This is a format string with two separate time ranges
@@ -1583,7 +1598,7 @@ msgstr "%s,%s,%s"
 
 #: src/translations.js:185
 msgid "every day"
-msgstr "Ogni dì"
+msgstr "ogni dì"
 
 #. Translators:
 #. * This represents a range of days with a starting and ending day.
@@ -1596,11 +1611,11 @@ msgstr "%s-%s"
 
 #: src/translations.js:208
 msgid "public holidays"
-msgstr "Feriis"
+msgstr "festîfs"
 
 #: src/translations.js:210
 msgid "school holidays"
-msgstr "Vacancis di scuele"
+msgstr "vacancis di scuele"
 
 #. Translators:
 #. * This is a list with two time intervals, such as:
@@ -1618,7 +1633,7 @@ msgstr "%s, %s"
 
 #: src/translations.js:264
 msgid "not open"
-msgstr "No viert"
+msgstr "no viert"
 
 #. Translators:
 #. * This is a time interval with a starting and an ending time.
@@ -1655,54 +1670,161 @@ msgid "service"
 msgstr "servizi"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "No cognossût"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "Di precîs"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%f h"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%f min"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s km"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s m"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s mi"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s ft"
 
+#: src/transitplugins/openTripPlanner.js:1228
+#, javascript-format
+msgid "Continue on %s"
+msgstr "Continue su %s"
+
+#: src/transitplugins/openTripPlanner.js:1230
+msgid "Continue"
+msgstr "Continue"
+
+#: src/transitplugins/openTripPlanner.js:1233
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "Svolte a çampe su %s"
+
+#: src/transitplugins/openTripPlanner.js:1235
+msgid "Turn left"
+msgstr "Svolte a çampe"
+
+#: src/transitplugins/openTripPlanner.js:1238
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "Svolte di pôc a çampe su %s"
+
+#: src/transitplugins/openTripPlanner.js:1240
+msgid "Turn slightly left"
+msgstr "Svolte di pôc a çampe"
+
+#: src/transitplugins/openTripPlanner.js:1243
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "Svolte di bot a çampe su %s"
+
+#: src/transitplugins/openTripPlanner.js:1245
+msgid "Turn sharp left"
+msgstr "Svolte di bot a çampe"
+
+#: src/transitplugins/openTripPlanner.js:1248
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "Svolte a diestre su %s"
+
+#: src/transitplugins/openTripPlanner.js:1250
+msgid "Turn right"
+msgstr "Svolte a diestre"
+
+#: src/transitplugins/openTripPlanner.js:1253
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "Svolte di pôc a diestre su %s"
+
+#: src/transitplugins/openTripPlanner.js:1255
+msgid "Turn slightly right"
+msgstr "Svolte di pôc a diestre"
+
+#: src/transitplugins/openTripPlanner.js:1258
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "Svolte di bot a diestre su %s"
+
+#: src/transitplugins/openTripPlanner.js:1260
+msgid "Turn sharp right"
+msgstr "Svolte di bot a diestre"
+
+#: src/transitplugins/openTripPlanner.js:1266
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "Inte rotatorie, cjape la jessude %s"
+
+#: src/transitplugins/openTripPlanner.js:1268
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "Inte rotatorie, cjape la jessude par %s"
+
+#: src/transitplugins/openTripPlanner.js:1270
+msgid "Take the roundabout"
+msgstr "Cjape la rotatorie"
+
+#: src/transitplugins/openTripPlanner.js:1274
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "Cjape l'assensôr e jes a %s"
+
+#: src/transitplugins/openTripPlanner.js:1276
+msgid "Take the elevator"
+msgstr "Cjape l'assensôr"
+
+#: src/transitplugins/openTripPlanner.js:1280
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "Fâs une inversion a U a çampe su %s"
+
+#: src/transitplugins/openTripPlanner.js:1282
+msgid "Make a left u-turn"
+msgstr "Fâs une inversion a U a çampe"
+
+#: src/transitplugins/openTripPlanner.js:1285
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "Fâs une inversion a U a diestre su %s"
+
+#: src/transitplugins/openTripPlanner.js:1287
+msgid "Make a rigth u-turn"
+msgstr "Fâs une inversion a U a diestre"
+
+#~ msgid "Route search by OpenTripPlanner"
+#~ msgstr "Ricercje dal percors cun OpenTripPlanner"
+
 #~ msgid "Open with another application"
 #~ msgstr "Vierç cuntune altre aplicazion"
 
diff --git a/po/id.po b/po/id.po
index 59cc70b..57e8b15 100644
--- a/po/id.po
+++ b/po/id.po
@@ -8,16 +8,16 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gnome-maps gnome-3-28\n"
 "Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/gnome-maps/issues\n"
-"POT-Creation-Date: 2019-08-02 06:11+0000\n"
-"PO-Revision-Date: 2019-08-06 17:30+0700\n"
-"Last-Translator: Kukuh Syafaat <kukuhsyafaat@gnome.org>\n"
+"POT-Creation-Date: 2019-10-30 21:01+0000\n"
+"PO-Revision-Date: 2019-11-10 09:18+0700\n"
+"Last-Translator: Andika Triwidada <andika@gmail.com>\n"
 "Language-Team: Indonesian <gnome@i15n.org>\n"
 "Language: id\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.2.3\n"
-"Plural-Forms: nplurals=2; plural= n!=1;\n"
+"X-Generator: Poedit 2.2.4\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 #: data/org.gnome.Maps.appdata.xml.in:6
 msgid "GNOME Maps"
@@ -58,7 +58,7 @@ msgstr ""
 "\"Pub dekat Main Street, Boston\" atau \"Hotel dekat Alexanderplatz, Berlin"
 "\"."
 
-#: data/org.gnome.Maps.appdata.xml.in:59
+#: data/org.gnome.Maps.appdata.xml.in:92
 msgid "The GNOME Project"
 msgstr "Proyek GNOME"
 
@@ -71,7 +71,7 @@ msgstr "Proyek GNOME"
 #.
 #. Translators: This is the program name.
 #: data/org.gnome.Maps.desktop.in:4 data/ui/main-window.ui:26
-#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:509
+#: src/application.js:81 src/mainWindow.js:139 src/mainWindow.js:513
 msgid "Maps"
 msgstr "Peta"
 
@@ -642,26 +642,27 @@ msgstr "Jungkitkan kenampakan"
 msgid "Route search by GraphHopper"
 msgstr "Pencarian rute oleh GraphHopper"
 
-#: data/ui/sidebar.ui:296
-msgid "Route search by OpenTripPlanner"
-msgstr "Pencarian rute oleh OpenTripPlanner"
-
-#: data/ui/sidebar.ui:369
+#: data/ui/sidebar.ui:364
 msgid ""
-"Routing itineraries for public transit is provided by GNOME\n"
-"using timetable data obtained from transit companies or agencies.\n"
-"The companies and agencies can not be held responsible for the results "
-"shown.\n"
+"Routing itineraries for public transit is provided by third-party\n"
+"services.\n"
 "GNOME can not guarantee correctness of the itineraries and schedules shown.\n"
+"Note that some providers might not include all available modes of "
+"transportation,\n"
+"e.g. a national provider might not include airlines, and a local provider "
+"could\n"
+"miss regional trains.\n"
 "Names and brands shown are to be considered as registered trademarks when "
 "applicable."
 msgstr ""
-"Rute perjalanan untuk angkutan umum disediakan oleh GNOME \n"
-"menggunakan data jadwal yang diperoleh dari perusahaan angkutan atau agen.\n"
-"Perusahaan dan agen tidak dapat bertanggung jawab atas hasil yang "
-"ditampilkan.\n"
+"Rute perjalanan untuk angkutan umum disediakan oleh layanan pihak ketiga.\n"
 "GNOME tidak dapat menjamin kebenaran dari perjalanan dan jadwal yang "
 "ditampilkan.\n"
+"Perhatikan bawa beberapa penyedia mungkin tidak menyertakan semua mode "
+"transportasi\n"
+"yang tersedia, mis. sebuah penyedia nasional mungkin tidak menyertakan "
+"penerbangan,\n"
+"dan suatu penyedia lokal mungkin tidak punya jadwal kereta regional.\n"
 "Nama dan merek yang ditampilkan dianggap sebagai merek dagang terdaftar "
 "ketika berlaku."
 
@@ -719,6 +720,10 @@ msgstr "Kereta bawah tanah"
 msgid "Ferries"
 msgstr "Feri"
 
+#: data/ui/transit-options-panel.ui:152
+msgid "Airplanes"
+msgstr "Penerbangan"
+
 #: data/ui/user-location-bubble.ui:13 src/geoclue.js:118
 msgid "Current location"
 msgstr "Lokasi saat ini"
@@ -761,6 +766,10 @@ msgstr "Suatu path ke sebuah struktur direktori ubin lokal"
 msgid "Show the version of the program"
 msgstr "Tampilkan versi program"
 
+#: src/application.js:104
+msgid "Ignore network availability"
+msgstr "Abaikan ketersediaan jaringan"
+
 #: src/checkInDialog.js:167
 msgid "Select an account"
 msgstr "Pilih sebuah akun"
@@ -891,15 +900,15 @@ msgstr "galat penguraian"
 msgid "unknown geometry"
 msgstr "geometri tak dikenal"
 
-#: src/graphHopper.js:112 src/openTripPlanner.js:652
+#: src/graphHopper.js:112 src/transitPlan.js:192
 msgid "Route request failed."
 msgstr "Permintaan rute gagal."
 
-#: src/graphHopper.js:119 src/openTripPlanner.js:615
+#: src/graphHopper.js:119 src/transitPlan.js:184
 msgid "No route found."
 msgstr "Rute tak ditemukan."
 
-#: src/graphHopper.js:207
+#: src/graphHopper.js:207 src/transitplugins/openTripPlanner.js:1100
 msgid "Start!"
 msgstr "Mulai!"
 
@@ -907,25 +916,25 @@ msgstr "Mulai!"
 msgid "All Layer Files"
 msgstr "Semua Berkas Lapisan"
 
-#: src/mainWindow.js:442
+#: src/mainWindow.js:446
 msgid "Failed to connect to location service"
 msgstr "Gagal menyambung ke layanan lokasi"
 
-#: src/mainWindow.js:507
+#: src/mainWindow.js:511
 msgid "translator-credits"
 msgstr ""
-"Andika Triwidada <andika@gmail.com>, 2013-2016, 2018.\n"
+"Andika Triwidada <andika@gmail.com>, 2013-2016, 2018, 2019.\n"
 "Kukuh Syafaat <kukuhsyafaat@gnome.org>, 2017, 2018, 2019."
 
-#: src/mainWindow.js:510
+#: src/mainWindow.js:514
 msgid "A map application for GNOME"
 msgstr "Aplikasi peta bagi GNOME"
 
-#: src/mainWindow.js:521
+#: src/mainWindow.js:525
 msgid "Copyright © 2011 – 2019 Red Hat, Inc. and The GNOME Maps authors"
 msgstr "Hak Cipta © 2011 – 2019 Red Hat, Inc. dan Penulis GNOME Peta"
 
-#: src/mainWindow.js:541
+#: src/mainWindow.js:545
 #, javascript-format
 msgid "Map data by %s and contributors"
 msgstr "Data peta oleh %s dan kontributor"
@@ -935,7 +944,7 @@ msgstr "Data peta oleh %s dan kontributor"
 #. * the bare name of the tile provider, or a linkified URL if one
 #. * is available
 #.
-#: src/mainWindow.js:557
+#: src/mainWindow.js:561
 #, javascript-format
 msgid "Map tiles provided by %s"
 msgstr "Ubin peta disediakan oleh %s"
@@ -949,27 +958,23 @@ msgstr "Ubin peta disediakan oleh %s"
 #. * (i.e. "%2$s ... %1$s ..." for positioning the project URL
 #. * before the provider).
 #.
-#: src/mainWindow.js:586
+#: src/mainWindow.js:590
 #, javascript-format
 msgid "Search provided by %s using %s"
 msgstr "Pencarian disediakan oleh %s menggunakan %s"
 
-#: src/mapView.js:374
+#: src/mapView.js:375
 msgid "File type is not supported"
 msgstr "Jenis berkas tak didukung"
 
-#: src/mapView.js:381
+#: src/mapView.js:382
 msgid "Failed to open layer"
 msgstr "Gagal membuka layer"
 
-#: src/mapView.js:417
+#: src/mapView.js:418
 msgid "Failed to open GeoURI"
 msgstr "Gagal membuka GeoURI"
 
-#: src/openTripPlanner.js:648
-msgid "No timetable data found for this route."
-msgstr "Tidak ada data jadwal yang ditemukan untuk rute ini."
-
 #. setting the status in session.cancel_message still seems
 #. to always give status IO_ERROR
 #: src/osmConnection.js:436
@@ -1270,7 +1275,7 @@ msgstr "Akses kursi roda:"
 msgid "Phone:"
 msgstr "Telepon:"
 
-#: src/placeEntry.js:205
+#: src/placeEntry.js:209
 msgid "Failed to parse Geo URI"
 msgstr "Gagal mengurai URI Geo"
 
@@ -1383,11 +1388,16 @@ msgid "failed to load file"
 msgstr "gagal memuat berkas"
 
 #. Translators: %s is a time expression with the format "%f h" or "%f min"
-#: src/sidebar.js:293
+#: src/sidebar.js:296
 #, javascript-format
 msgid "Estimated time: %s"
 msgstr "Perkiraan waktu: %s"
 
+#: src/sidebar.js:352
+#, javascript-format
+msgid "Itineraries provided by %s"
+msgstr "Jadwal disediakan oleh %s"
+
 #. Translators: this is a format string indicating instructions
 #. * starting a journey at the address given as the parameter
 #.
@@ -1420,7 +1430,7 @@ msgstr "Berjalan %s"
 msgid "Arrive at %s"
 msgstr "Tiba di %s"
 
-#: src/transit.js:77
+#: src/transit.js:77 src/transitplugins/openTripPlanner.js:1113
 msgid "Arrive"
 msgstr "Tiba"
 
@@ -1452,17 +1462,25 @@ msgstr "Tidak ada alternatif lain yang ditemukan."
 #. * Translators: this is a format string giving the equivalent to
 #. * "may 29" according to the current locale's convensions.
 #.
-#: src/transitOptionsPanel.js:141
+#: src/transitOptionsPanel.js:143
 msgctxt "month-day-date"
 msgid "%b %e"
 msgstr "%e %b"
 
+#: src/transitPlan.js:188
+msgid "No timetable data found for this route."
+msgstr "Tidak ada data jadwal yang ditemukan untuk rute ini."
+
+#: src/transitPlan.js:196
+msgid "No provider found for this route."
+msgstr "Tidak ada penyedia yang ditemukan untuk rute ini."
+
 #. Translators: this is a format string for showing a departure and
 #. * arrival time, like:
 #. * "12:00 – 13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:254
+#: src/transitPlan.js:313
 #, javascript-format
 msgid "%s – %s"
 msgstr "%s – %s"
@@ -1471,7 +1489,7 @@ msgstr "%s – %s"
 #. * less than an hour, with only the minutes part, using plural forms
 #. * as appropriate
 #.
-#: src/transitPlan.js:281
+#: src/transitPlan.js:340
 #, javascript-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1482,7 +1500,7 @@ msgstr[1] "%d menit"
 #. * where the duration is an exact number of hours (i.e. no
 #. * minutes part), using plural forms as appropriate
 #.
-#: src/transitPlan.js:292
+#: src/transitPlan.js:351
 #, javascript-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1493,7 +1511,7 @@ msgstr[1] "%d jam"
 #. * where the duration contains an hour and minute part, it's
 #. * pluralized on the hours part
 #.
-#: src/transitPlan.js:298
+#: src/transitPlan.js:357
 #, javascript-format
 msgid "%d:%02d hour"
 msgid_plural "%d:%02d hours"
@@ -1506,7 +1524,7 @@ msgstr[1] "%d:%02d jam"
 #. * "12:00–13:03" where the placeholder %s are the actual times,
 #. * these could be rearranged if needed.
 #.
-#: src/transitPlan.js:651
+#: src/transitPlan.js:750
 #, javascript-format
 msgid "%s–%s"
 msgstr "%s–%s"
@@ -1660,75 +1678,154 @@ msgid "service"
 msgstr "pelayanan"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:220
+#: src/utils.js:229
 msgid "Unknown"
 msgstr "Tak diketahui"
 
 #. Translators: Accuracy of user location information
-#: src/utils.js:223
+#: src/utils.js:232
 msgid "Exact"
 msgstr "Eksak"
 
-#: src/utils.js:281
+#: src/utils.js:290
 #, javascript-format
 msgid "%f h"
 msgstr "%f j"
 
-#: src/utils.js:283
+#: src/utils.js:292
 #, javascript-format
 msgid "%f min"
 msgstr "%f men"
 
-#: src/utils.js:285
+#: src/utils.js:294
 #, javascript-format
 msgid "%f s"
 msgstr "%f s"
 
 #. Translators: This is a distance measured in kilometers
-#: src/utils.js:296
+#: src/utils.js:305
 #, javascript-format
 msgid "%s km"
 msgstr "%s km"
 
 #. Translators: This is a distance measured in meters
-#: src/utils.js:299
+#: src/utils.js:308
 #, javascript-format
 msgid "%s m"
 msgstr "%s m"
 
 #. Translators: This is a distance measured in miles
-#: src/utils.js:307
+#: src/utils.js:316
 #, javascript-format
 msgid "%s mi"
 msgstr "%s mil"
 
 #. Translators: This is a distance measured in feet
-#: src/utils.js:310
+#: src/utils.js:319
 #, javascript-format
 msgid "%s ft"
 msgstr "%s kaki"
 
-#~ msgid ""
-#~ "Show live-updated thumbnails for the street/aerial layer switcher, "
-#~ "instead of (outdated) hard-code thumbnails"
-#~ msgstr ""
-#~ "Tampilkan gambar mini yang diperbarui secara langsung untuk pengalih "
-#~ "lapisan jalan/udara, bukan gambar mini kode lama"
+#: src/transitplugins/openTripPlanner.js:1174
+#, javascript-format
+msgid "Continue on %s"
+msgstr "Lanjutkan pada %s"
+
+#: src/transitplugins/openTripPlanner.js:1176
+msgid "Continue"
+msgstr "Lanjutkan"
+
+#: src/transitplugins/openTripPlanner.js:1179
+#, javascript-format
+msgid "Turn left on %s"
+msgstr "Belok kiri di %s"
 
-#~ msgid "Move app menu to the headerbar"
-#~ msgstr "Pindahkan menu aplikasi ke bilah tajuk"
+#: src/transitplugins/openTripPlanner.js:1181
+msgid "Turn left"
+msgstr "Belok kiri"
 
-#~ msgid "Updated application icon"
-#~ msgstr "Ikon aplikasi yang dimutakhirkan"
+#: src/transitplugins/openTripPlanner.js:1184
+#, javascript-format
+msgid "Turn slightly left on %s"
+msgstr "Belok sedikit ke kiri pada %s"
 
-#~ msgid "Press enter to search"
-#~ msgstr "Tekan enter untuk mencari"
+#: src/transitplugins/openTripPlanner.js:1186
+msgid "Turn slightly left"
+msgstr "Belok sedikit ke kiri"
 
-#~ msgid "org.gnome.Maps"
-#~ msgstr "org.gnome.Maps"
+#: src/transitplugins/openTripPlanner.js:1189
+#, javascript-format
+msgid "Turn sharp left on %s"
+msgstr "Belok kiri tajam pada %s"
 
-#~ msgid "Quit"
-#~ msgstr "Keluar"
+#: src/transitplugins/openTripPlanner.js:1191
+msgid "Turn sharp left"
+msgstr "Belok kiri tajam"
+
+#: src/transitplugins/openTripPlanner.js:1194
+#, javascript-format
+msgid "Turn right on %s"
+msgstr "Belok kanan pada %s"
+
+#: src/transitplugins/openTripPlanner.js:1196
+msgid "Turn right"
+msgstr "Belok kanan"
+
+#: src/transitplugins/openTripPlanner.js:1199
+#, javascript-format
+msgid "Turn slightly right on %s"
+msgstr "Belok sedikit ke kanan pada %s"
+
+#: src/transitplugins/openTripPlanner.js:1201
+msgid "Turn slightly right"
+msgstr "Belok sedikit ke kanan"
+
+#: src/transitplugins/openTripPlanner.js:1204
+#, javascript-format
+msgid "Turn sharp right on %s"
+msgstr "Belok kanan tajam pada %s"
+
+#: src/transitplugins/openTripPlanner.js:1206
+msgid "Turn sharp right"
+msgstr "Belok kanan tajam"
+
+#: src/transitplugins/openTripPlanner.js:1212
+#, javascript-format
+msgid "In the roundaboat, take exit %s"
+msgstr "Di bundaran, ambil jalur keluar %s"
+
+#: src/transitplugins/openTripPlanner.js:1214
+#, javascript-format
+msgid "In the roundabout, take exit to %s"
+msgstr "Di bundaran, ambil jalur ke %s"
+
+#: src/transitplugins/openTripPlanner.js:1216
+msgid "Take the roundabout"
+msgstr "Ambil bundaran"
+
+#: src/transitplugins/openTripPlanner.js:1220
+#, javascript-format
+msgid "Take the elevator and get off at %s"
+msgstr "Naik lift dan turun di %s"
+
+#: src/transitplugins/openTripPlanner.js:1222
+msgid "Take the elevator"
+msgstr "Naik lift"
+
+#: src/transitplugins/openTripPlanner.js:1226
+#, javascript-format
+msgid "Make a left u-turn onto %s"
+msgstr "Putar balik kiri ke %s"
+
+#: src/transitplugins/openTripPlanner.js:1228
+msgid "Make a left u-turn"
+msgstr "Putar balik kiri"
+
+#: src/transitplugins/openTripPlanner.js:1231
+#, javascript-format
+msgid "Make a right u-turn onto %s"
+msgstr "Putar balik kanan ke %s"
 
-#~ msgid "OK"
-#~ msgstr "OK"
+#: src/transitplugins/openTripPlanner.js:1233
+msgid "Make a rigth u-turn"
+msgstr "Putar balik kanan"
diff --git a/src/application.js b/src/application.js
index 58d15c3..a4bee15 100644
--- a/src/application.js
+++ b/src/application.js
@@ -97,6 +97,13 @@ var Application = GObject.registerClass({
         this.add_main_option('version', 'v'.charCodeAt(0), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                              _("Show the version of the program"), null);
 
+        this.add_main_option('force-online',
+                             0,
+                             GLib.OptionFlags.NONE,
+                             GLib.OptionArg.NONE,
+                             _("Ignore network availability"),
+                             null);
+
         this.connect('handle-local-options', (app, options) => {
             if (options.contains('local')) {
                 let variant = options.lookup_value('local', null);
@@ -108,6 +115,8 @@ var Application = GObject.registerClass({
                  * leaving the running instance unaffected
                  */
                 return 0;
+            } else if (options.contains('force-online')) {
+                this._forceOnline = true;
             }
 
             return -1;
@@ -115,7 +124,9 @@ var Application = GObject.registerClass({
     }
 
     _checkNetwork() {
-        this.connected = networkMonitor.connectivity === Gio.NetworkConnectivity.FULL;
+        this.connected =
+            this._forceOnline ||
+            networkMonitor.connectivity === Gio.NetworkConnectivity.FULL;
     }
 
     _showContact(id) {
diff --git a/src/color.js b/src/color.js
index 7216a28..feb630a 100644
--- a/src/color.js
+++ b/src/color.js
@@ -20,7 +20,7 @@
  */
 
 /* Minimum contrast ratio for foreground/background color for i.e. route labels */
-const MIN_CONTRAST_RATIO = 3.0;
+const MIN_CONTRAST_RATIO = 2.0;
 
 /**
  * Parses a given color component index (0: red, 1: green, 2: blue)
diff --git a/src/graphHopperGeocode.js b/src/graphHopperGeocode.js
index 8cb3bd4..e94442c 100644
--- a/src/graphHopperGeocode.js
+++ b/src/graphHopperGeocode.js
@@ -144,9 +144,7 @@ var GraphHopperGeocode = class {
 
     _readService() {
         let graphHopperGeocode = Service.getService().graphHopperGeocode;
-        let locale = GLib.get_language_names()[0];
-        // the last item returned is the "bare" language
-        this._language = GLib.get_locale_variants(locale).slice(-1)[0];
+        this._language = Utils.getLanguage();
 
         if (graphHopperGeocode) {
             this._baseUrl = graphHopperGeocode.baseUrl;
diff --git a/src/graphHopperTransit.js b/src/graphHopperTransit.js
new file mode 100644
index 0000000..7fa927e
--- /dev/null
+++ b/src/graphHopperTransit.js
@@ -0,0 +1,163 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2019 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+/**
+ * Utilities to use GraphHopper to perform walking routes for use in
+ * transit itineraries, for plugins not natively supporting turn-by-turn
+ * routing for walking legs
+ */
+
+const Champlain = imports.gi.Champlain;
+
+const Application = imports.application;
+const Location = imports.location;
+const Place = imports.place;
+const RouteQuery = imports.routeQuery;
+const TransitPlan = imports.transitPlan;
+
+/* Creates a new walking leg given start and end places, and a route
+ * obtained from GraphHopper. If the route is undefined (which happens if
+ * GraphHopper failed to obtain a walking route, approximate it with a
+ * straight line. */
+function createWalkingLeg(from, to, fromName, toName, route) {
+    let fromLocation = from.place.location;
+    let toLocation = to.place.location;
+    let fromCoordinate = [fromLocation.latitude, fromLocation.longitude];
+    let toCoordinate = [toLocation.latitude, toLocation.longitude];
+    let polyline = route ? route.path :
+                           createStraightPolyline(fromLocation, toLocation);
+    let distance = route ? route.distance :
+                           fromLocation.get_distance_from(toLocation) * 1000;
+    /* as an estimate for approximated straight-line walking legs,
+     * assume a speed of 1 m/s to allow some extra time */
+    let duration = route ? route.time / 1000 : distance;
+    let walkingInstructions = route ? route.turnPoints : null;
+
+    return new TransitPlan.Leg({ fromCoordinate: fromCoordinate,
+                                 toCoordinate: toCoordinate,
+                                 from: fromName,
+                                 to: toName,
+                                 isTransit: false,
+                                 polyline: polyline,
+                                 duration: duration,
+                                 distance: distance,
+                                 walkingInstructions: walkingInstructions });
+}
+
+// create a straight-line "as the crow flies" polyline between two places
+function createStraightPolyline(fromLoc, toLoc) {
+    return [new Champlain.Coordinate({ latitude: fromLoc.latitude,
+                                       longitude: fromLoc.longitude }),
+            new Champlain.Coordinate({ latitude: toLoc.latitude,
+                                       longitude: toLoc.longitude })];
+}
+
+var _walkingRoutes = [];
+
+/* fetches walking route and stores the route for the given coordinate
+ * pair to avoid requesting the same route over and over from GraphHopper
+ */
+function fetchWalkingRoute(points, callback) {
+    let index = points[0].place.location.latitude + ',' +
+                points[0].place.location.longitude + ';' +
+                points[1].place.location.latitude + ',' +
+                points[1].place.location.longitude;
+    let route = _walkingRoutes[index];
+
+    if (!route) {
+        Application.routingDelegator.graphHopper.fetchRouteAsync(points,
+                                          RouteQuery.Transportation.PEDESTRIAN,
+                                          (newRoute) => {
+            _walkingRoutes[index] = newRoute;
+            callback(newRoute);
+        });
+    } else {
+        callback(route);
+    }
+}
+
+// create a query point from a bare coordinate (lat, lon pair)
+function createQueryPointForCoord(coord) {
+    let location = new Location.Location({ latitude: coord[0],
+                                           longitude: coord[1],
+                                           accuracy: 0 });
+    let place = new Place.Place({ location: location });
+    let point = new RouteQuery.QueryPoint();
+
+    point.place = place;
+    return point;
+}
+
+/**
+ * Refine itineraries with walking legs retrieved from GraphHopper.
+ * Intended for use by transit plugins where the source API doesn't give
+ * full walking turn-by-turn routing
+ */
+function addWalkingToItineraries(itineraries, callback) {
+    _addWalkingToItinerariesRecursive(itineraries, 0, callback);
+}
+
+function _addWalkingToItinerariesRecursive(itineraries, index, callback) {
+    if (index === itineraries.length) {
+        callback();
+    } else {
+        let itinerary = itineraries[index];
+
+        _addWalkingToLegsRecursive(itinerary.legs, 0, () => {
+            _addWalkingToItinerariesRecursive(itineraries, index + 1, callback);
+        });
+    }
+}
+
+function _addWalkingToLegsRecursive(legs, index, callback) {
+    if (index === legs.length) {
+        callback();
+    } else {
+        let leg = legs[index];
+
+        if (!leg.transit) {
+            let from = createQueryPointForCoord(leg.fromCoordinate);
+            let to = createQueryPointForCoord(leg.toCoordinate);
+
+            fetchWalkingRoute([from, to], (route) => {
+                if (route) {
+                    let duration = route.time / 1000;
+
+                    /* for walking legs not in the start or end
+                     * only replace with the retrieved one if it's not
+                     * longer in duration that the previous (straight-line)
+                     * one.
+                     */
+                    if (index === 0 || index === legs.length - 1 ||
+                        duration <= leg.duration) {
+                        leg.distance = route.distance;
+                        leg.walkingInstructions = route.turnPoints;
+                        leg.polyline = route.path;
+                    }
+                }
+
+                _addWalkingToLegsRecursive(legs, index + 1, callback);
+            });
+        } else {
+            _addWalkingToLegsRecursive(legs, index + 1, callback);
+        }
+    }
+}
diff --git a/src/mapView.js b/src/mapView.js
index 6784a7c..6197f0e 100644
--- a/src/mapView.js
+++ b/src/mapView.js
@@ -262,7 +262,7 @@ var MapView = GObject.registerClass({
 
     _connectRouteSignals() {
         let route = Application.routingDelegator.graphHopper.route;
-        let transitPlan = Application.routingDelegator.openTripPlanner.plan;
+        let transitPlan = Application.routingDelegator.transitRouter.plan;
         let query = Application.routeQuery;
 
         route.connect('update', () => {
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index 337fe13..1947597 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -24,6 +24,7 @@
     <file>gpxShapeLayer.js</file>
     <file>graphHopper.js</file>
     <file>graphHopperGeocode.js</file>
+    <file>graphHopperTransit.js</file>
     <file>hvt.js</file>
     <file>http.js</file>
     <file>instructionRow.js</file>
@@ -39,7 +40,6 @@
     <file>mapSource.js</file>
     <file>mapView.js</file>
     <file>mapWalker.js</file>
-    <file>openTripPlanner.js</file>
     <file>osmAccountDialog.js</file>
     <file>osmConnection.js</file>
     <file>osmEdit.js</file>
@@ -91,8 +91,10 @@
     <file>transitOptionsPanel.js</file>
     <file>transitPlan.js</file>
     <file>transitPrintLayout.js</file>
+    <file>transitRouter.js</file>
     <file>transitRouteLabel.js</file>
     <file>transitStopRow.js</file>
+    <file>transitTweaks.js</file>
     <file>transitWalkMarker.js</file>
     <file>translations.js</file>
     <file>turnPointMarker.js</file>
@@ -111,5 +113,7 @@
     <file alias="geojsonvt/tile.js">tile.js</file>
     <file alias="geojsonvt/transform.js">transform.js</file>
     <file alias="geojsonvt/wrap.js">wrap.js</file>
+    <file>transitplugins/openTripPlanner.js</file>
+    <file>transitplugins/resrobot.js</file>
   </gresource>
 </gresources>
diff --git a/src/osmConnection.js b/src/osmConnection.js
index 6b3b7ac..f784251 100644
--- a/src/osmConnection.js
+++ b/src/osmConnection.js
@@ -47,7 +47,7 @@ const SECRET_SCHEMA = new Secret.Schema("org.gnome.Maps",
 var OSMConnection = class OSMConnection {
 
     constructor() {
-        this._session = new Soup.Session();
+        this._session = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version });
 
         /* OAuth proxy used for making OSM uploads */
         this._callProxy = Rest.OAuthProxy.new(CONSUMER_KEY, CONSUMER_SECRET,
diff --git a/src/overpass.js b/src/overpass.js
index 25dd31e..37781a8 100644
--- a/src/overpass.js
+++ b/src/overpass.js
@@ -70,7 +70,7 @@ var Overpass = GObject.registerClass({
         this.outputSortOrder = params.outputSortOrder || _DEFAULT_OUTPUT_SORT_ORDER;
 
         // HTTP Session Variables
-        this._session = new Soup.Session();
+        this._session = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version });
     }
 
     addInfo(place) {
diff --git a/src/photonGeocode.js b/src/photonGeocode.js
index a28cd15..980e9b4 100644
--- a/src/photonGeocode.js
+++ b/src/photonGeocode.js
@@ -149,9 +149,7 @@ var PhotonGeocode = class {
 
     _readService() {
         let photon = Service.getService().photonGeocode;
-        let locale = GLib.get_language_names()[0];
-        // the last item returned is the "bare" language
-        let language = GLib.get_locale_variants(locale).slice(-1)[0];
+        let language = Utils.getLanguage();
         let supportedLanguages;
 
         if (photon) {
diff --git a/src/printOperation.js b/src/printOperation.js
index 89462a5..b3307c0 100644
--- a/src/printOperation.js
+++ b/src/printOperation.js
@@ -57,7 +57,7 @@ var PrintOperation = class PrintOperation {
     _beginPrint(operation, context, data) {
         let route = Application.routingDelegator.graphHopper.route;
         let selectedTransitItinerary =
-            Application.routingDelegator.openTripPlanner.plan.selectedItinerary;
+            Application.routingDelegator.transitRouter.plan.selectedItinerary;
         let width = context.get_width();
         let height = context.get_height();
 
diff --git a/src/route.js b/src/route.js
index 1129dec..458ed2b 100644
--- a/src/route.js
+++ b/src/route.js
@@ -38,7 +38,10 @@ var TurnPointType = {
 
     // This one is not in GraphHopper, so choose
     // a reasonably unlikely number for this
-    START:         10000
+    START:         10000,
+    ELEVATOR:      10001,
+    UTURN_LEFT:    10002,
+    UTURN_RIGHT:   10003
 };
 
 /* countries/terrotories driving on the left
diff --git a/src/routeQuery.js b/src/routeQuery.js
index 2fbedf0..060015d 100644
--- a/src/routeQuery.js
+++ b/src/routeQuery.js
@@ -239,7 +239,8 @@ var RouteQuery = GObject.registerClass({
     }
 
     isValid() {
-        if (this.filledPoints.length >= 2)
+        if (this.filledPoints.length >= 2 &&
+            this.filledPoints.length === this.points.length)
             return true;
         else
             return false;
diff --git a/src/routingDelegator.js b/src/routingDelegator.js
index 6e02b21..37728c7 100644
--- a/src/routingDelegator.js
+++ b/src/routingDelegator.js
@@ -20,7 +20,7 @@
  */
 
 const GraphHopper = imports.graphHopper;
-const OpenTripPlanner = imports.openTripPlanner;
+const TransitRouter = imports.transitRouter;
 const RouteQuery = imports.routeQuery;
 
 const _FALLBACK_TRANSPORTATION = RouteQuery.Transportation.PEDESTRIAN;
@@ -32,16 +32,14 @@ var RoutingDelegator = class RoutingDelegator {
 
         this._transitRouting = false;
         this._graphHopper = new GraphHopper.GraphHopper({ query: this._query });
-        this._openTripPlanner =
-            new OpenTripPlanner.OpenTripPlanner({ query: this._query,
-                                                  graphHopper: this._graphHopper });
+        this._transitRouter = new TransitRouter.TransitRouter({ query: this._query });
         this._query.connect('notify::points', this._onQueryChanged.bind(this));
 
         /* if the query is set to transit mode when it's not available, revert
          * to a fallback mode
          */
         if (this._query.transportation === RouteQuery.Transportation.TRANSIT &&
-            !this._openTripPlanner.enabled) {
+            !this._transitRouter.enabled) {
             this._query.transportation = _FALLBACK_TRANSPORTATION;
         }
     }
@@ -50,8 +48,8 @@ var RoutingDelegator = class RoutingDelegator {
         return this._graphHopper;
     }
 
-    get openTripPlanner() {
-        return this._openTripPlanner;
+    get transitRouter() {
+        return this._transitRouter;
     }
 
     set useTransit(useTransit) {
@@ -60,7 +58,7 @@ var RoutingDelegator = class RoutingDelegator {
 
     reset() {
         if (this._transitRouting)
-            this._openTripPlanner.plan.reset();
+            this._transitRouter.plan.reset();
         else
             this._graphHopper.route.reset();
     }
@@ -68,7 +66,7 @@ var RoutingDelegator = class RoutingDelegator {
     _onQueryChanged() {
         if (this._query.isValid()) {
             if (this._transitRouting) {
-                this._openTripPlanner.fetchFirstResults();
+                this._transitRouter.fetchFirstResults();
             } else {
                 this._graphHopper.fetchRoute(this._query.filledPoints,
                                              this._query.transportation);
diff --git a/src/sidebar.js b/src/sidebar.js
index 1b546a2..3294665 100644
--- a/src/sidebar.js
+++ b/src/sidebar.js
@@ -64,7 +64,8 @@ var Sidebar = GObject.registerClass({
                         'transitItineraryListBox',
                         'transitItineraryBackButton',
                         'transitItineraryTimeLabel',
-                        'transitItineraryDurationLabel']
+                        'transitItineraryDurationLabel',
+                        'transitAttributionLabel']
 }, class Sidebar extends Gtk.Revealer {
 
     _init(mapView) {
@@ -95,13 +96,13 @@ var Sidebar = GObject.registerClass({
         this._query.addPoint(1);
         this._switchRoutingMode(Application.routeQuery.transportation);
         /* Enable/disable transit mode switch based on the presence of
-         * OpenTripPlanner.
+         * public transit providers.
          * For some reason, setting visible to false in the UI file and
          * dynamically setting visible false here doesn't work, maybe because
          * it's part of a radio group? As a workaround, just remove the button
          * instead.
          */
-        if (!Application.routingDelegator.openTripPlanner.enabled)
+        if (!Application.routingDelegator.transitRouter.enabled)
             this._modeTransitToggle.destroy();
     }
 
@@ -147,14 +148,14 @@ var Sidebar = GObject.registerClass({
     _switchRoutingMode(mode) {
         if (mode === RouteQuery.Transportation.TRANSIT) {
             Application.routingDelegator.useTransit = true;
-            this._linkButtonStack.visible_child_name = 'openTripPlanner';
+            this._linkButtonStack.visible_child_name = 'transit';
             this._transitOptionsPanel.reset();
             this._transitRevealer.reveal_child = true;
         } else {
             Application.routingDelegator.useTransit = false;
-            this._linkButtonStack.visible_child_name = 'graphHopper';
+            this._linkButtonStack.visible_child_name = 'turnByTurn';
             this._transitRevealer.reveal_child = false;
-            Application.routingDelegator.openTripPlanner.plan.deselectItinerary();
+            Application.routingDelegator.transitRouter.plan.deselectItinerary();
         }
         this._clearInstructions();
     }
@@ -214,7 +215,7 @@ var Sidebar = GObject.registerClass({
 
     _initInstructionList() {
         let route = Application.routingDelegator.graphHopper.route;
-        let transitPlan = Application.routingDelegator.openTripPlanner.plan;
+        let transitPlan = Application.routingDelegator.transitRouter.plan;
 
         route.connect('reset', () => {
             this._clearInstructions();
@@ -232,6 +233,7 @@ var Sidebar = GObject.registerClass({
             /* don't remove query points as with the turn-based routing,
              * since we might get "no route" because of the time selected
              * and so on */
+            this._transitAttributionLabel.label = '';
         });
 
         transitPlan.connect('no-more-results', () => {
@@ -248,6 +250,7 @@ var Sidebar = GObject.registerClass({
                 if (this._query.transportation === RouteQuery.Transportation.TRANSIT) {
                     this._clearTransitOverview();
                     this._showTransitOverview();
+                    this._transitAttributionLabel.label = '';
                 } else {
                     this._clearInstructions();
                 }
@@ -300,6 +303,7 @@ var Sidebar = GObject.registerClass({
         });
 
         transitPlan.connect('update', () => {
+            this._updateTransitAttribution();
             this._clearTransitOverview();
             this._showTransitOverview();
             this._populateTransitItineraryOverview();
@@ -340,8 +344,26 @@ var Sidebar = GObject.registerClass({
         listBox.forall(listBox.remove.bind(listBox));
     }
 
+    _updateTransitAttribution() {
+        let plan = Application.routingDelegator.transitRouter.plan;
+
+        if (plan.attribution) {
+            let attributionLabel =
+                _("Itineraries provided by %s").format(plan.attribution);
+            if (plan.attributionUrl) {
+                this._transitAttributionLabel.label =
+                    '<a href="%s">%s</a>'.format([plan.attributionUrl],
+                                                 attributionLabel);
+            } else {
+                this._transitAttributionLabel.label = attributionLabel;
+            }
+        } else {
+            this._transitAttributionLabel.label = '';
+        }
+    }
+
     _showTransitOverview() {
-        let plan = Application.routingDelegator.openTripPlanner.plan;
+        let plan = Application.routingDelegator.transitRouter.plan;
 
         this._transitListStack.visible_child_name = 'overview';
         this._transitHeader.visible_child_name = 'options';
@@ -354,7 +376,7 @@ var Sidebar = GObject.registerClass({
     }
 
     _populateTransitItineraryOverview() {
-        let plan = Application.routingDelegator.openTripPlanner.plan;
+        let plan = Application.routingDelegator.transitRouter.plan;
 
         plan.itineraries.forEach((itinerary) => {
             let row =
@@ -371,7 +393,7 @@ var Sidebar = GObject.registerClass({
     }
 
     _onItineraryActivated(itinerary) {
-        let plan = Application.routingDelegator.openTripPlanner.plan;
+        let plan = Application.routingDelegator.transitRouter.plan;
 
         this._populateTransitItinerary(itinerary);
         this._showTransitItineraryView();
@@ -380,7 +402,7 @@ var Sidebar = GObject.registerClass({
 
     _onMoreActivated(row) {
         row.startLoading();
-        Application.routingDelegator.openTripPlanner.fetchMoreResults();
+        Application.routingDelegator.transitRouter.fetchMoreResults();
     }
 
     _onItineraryOverviewRowActivated(listBox, row) {
diff --git a/src/transitItineraryRow.js b/src/transitItineraryRow.js
index b4b1878..6c5c965 100644
--- a/src/transitItineraryRow.js
+++ b/src/transitItineraryRow.js
@@ -24,6 +24,9 @@ const Gtk = imports.gi.Gtk;
 
 const TransitRouteLabel = imports.transitRouteLabel;
 
+// maximum number of legs to show before abbreviating with a … in the middle
+const MAX_LEGS_SHOWN = 8;
+
 var TransitItineraryRow = GObject.registerClass({
     Template: 'resource:///org/gnome/Maps/ui/transit-itinerary-row.ui',
     InternalChildren: ['timeLabel',
@@ -54,12 +57,31 @@ var TransitItineraryRow = GObject.registerClass({
          */
         let useCompact = length > 2;
         /* don't show the route labels if too much space is consumed,
-         * the constant 28 here was empiracally tested out...
+         * the constant 26 here was empiracally tested out...
          */
         let estimatedSpace = this._calculateEstimatedSpace();
-        let useContractedLabels = estimatedSpace > 28;
+        let useContractedLabels = estimatedSpace > 26;
+
+        if (length > MAX_LEGS_SHOWN) {
+            /* ellipsize list with horizontal dots to avoid overflowing and
+             * expanding the sidebar
+             */
+            this._renderLegs(this._itinerary.legs.slice(0, MAX_LEGS_SHOWN / 2),
+                             true, true);
+            this._summaryGrid.add(new Gtk.Label({ visible: true,
+                                                  label: '\u22ef' } ));
+            this._renderLegs(this._itinerary.legs.slice(-MAX_LEGS_SHOWN / 2),
+                             true, true);
+        } else {
+            this._renderLegs(this._itinerary.legs, useCompact,
+                             useContractedLabels);
+        }
+    }
+
+    _renderLegs(legs, useCompact, useContractedLabels) {
+        let length = legs.length;
 
-        this._itinerary.legs.forEach((leg, i) => {
+        legs.forEach((leg, i) =>  {
             this._summaryGrid.add(this._createLeg(leg, useCompact,
                                                   useContractedLabels));
             if (i !== length - 1)
diff --git a/src/transitOptionsPanel.js b/src/transitOptionsPanel.js
index daa8b31..3039b40 100644
--- a/src/transitOptionsPanel.js
+++ b/src/transitOptionsPanel.js
@@ -25,6 +25,7 @@ const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
 
 const Application = imports.application;
+const HVT = imports.hvt;
 const Time = imports.time;
 const TransitOptions = imports.transitOptions;
 const TransitPlan = imports.transitPlan;
@@ -46,7 +47,8 @@ var TransitOptionsPanel = GObject.registerClass({
                        'tramCheckButton',
                        'trainCheckButton',
                        'subwayCheckButton',
-                       'ferryCheckButton']
+                       'ferryCheckButton',
+                       'airplaneCheckButton']
 }, class TransitOptionsPanel extends Gtk.Grid {
 
     _init(params) {
@@ -88,8 +90,8 @@ var TransitOptionsPanel = GObject.registerClass({
             this._transitTimeEntry.visible = false;
             this._transitDateButton.visible = false;
             this._query.arriveBy = false;
-            this._query.time = null;
             this._query.date = null;
+            this._query.time = null;
             this._timeSelected = null;
             this._dateSelected = null;
         } else {
@@ -171,9 +173,10 @@ var TransitOptionsPanel = GObject.registerClass({
         let trainSelected = this._trainCheckButton.active;
         let subwaySelected = this._subwayCheckButton.active;
         let ferrySelected = this._ferryCheckButton.active;
+        let airplaneSelected = this._airplaneCheckButton.active;
 
         if (busSelected && tramSelected && trainSelected && subwaySelected &&
-            ferrySelected) {
+            ferrySelected && airplaneSelected) {
             options.showAllTransitTypes = true;
         } else {
             if (busSelected)
@@ -186,6 +189,8 @@ var TransitOptionsPanel = GObject.registerClass({
                 options.addTransitType(TransitPlan.RouteType.SUBWAY);
             if (ferrySelected)
                 options.addTransitType(TransitPlan.RouteType.FERRY);
+            if (airplaneSelected)
+                options.addTransitType(HVT.AIR_SERVICE);
         }
 
         return options;
diff --git a/src/transitPlan.js b/src/transitPlan.js
index 6953394..a63c07e 100644
--- a/src/transitPlan.js
+++ b/src/transitPlan.js
@@ -96,6 +96,8 @@ var Plan = GObject.registerClass({
     _init(params) {
         super._init(params);
         this.reset();
+        this._attribution = null;
+        this._attributionUrl = null;
     }
 
     get itineraries() {
@@ -106,16 +108,57 @@ var Plan = GObject.registerClass({
         return this._selectedItinerary;
     }
 
+    get attribution() {
+        return this._attribution;
+    }
+
+    set attribution(attribution) {
+        this._attribution = attribution;
+    }
+
+    get attributionUrl() {
+        return this._attributionUrl;
+    }
+
+    set attributionUrl(attributionUrl) {
+        this._attributionUrl = attributionUrl;
+    }
+
     update(itineraries) {
         this._itineraries = itineraries;
         this.bbox = this._createBBox();
         this.emit('update');
     }
 
+    /**
+     * Update plan with new itineraries, setting the new itineraries if it's
+     * the first fetch for a query, or extending the existing ones if it's
+     * a request to load more
+     */
+    updateWithNewItineraries(itineraries, arriveBy, extendPrevious) {
+        /* sort itineraries, by departure time ascending if querying
+         * by leaving time, by arrival time descending when querying
+         * by arriving time
+         */
+        if (arriveBy)
+            itineraries.sort(sortItinerariesByArrivalDesc);
+        else
+            itineraries.sort(sortItinerariesByDepartureAsc);
+
+        let newItineraries =
+            extendPrevious ? this.itineraries.concat(itineraries) : itineraries;
+
+        this.update(newItineraries);
+    }
+
+
+
     reset() {
         this._itineraries = [];
         this.bbox = null;
         this._selectedItinerary = null;
+        this._attribution = null;
+        this._attributionUrl = null;
         this.emit('reset');
     }
 
@@ -137,6 +180,22 @@ var Plan = GObject.registerClass({
         this.emit('error', msg);
     }
 
+    noRouteFound() {
+        this.emit('error', _("No route found."));
+    }
+
+    noTimetable() {
+        this.emit('error', _("No timetable data found for this route."));
+    }
+
+    requestFailed() {
+        this.emit('error', _("Route request failed."));
+    }
+
+    noProvider() {
+        this.emit('error', _("No provider found for this route."));
+    }
+
     _createBBox() {
         let bbox = new Champlain.BoundingBox();
         this._itineraries.forEach(function(itinerary) {
@@ -348,6 +407,10 @@ class Itinerary extends GObject.Object {
     get transitArrivalTimezoneOffset() {
         return this._getTransitArrivalLeg().timezoneOffset;
     }
+
+    get isWalkingOnly() {
+        return this.legs.length === 1 && !this.legs[0].isTransit;
+    }
 });
 
 var Leg = class Leg {
@@ -425,6 +488,10 @@ var Leg = class Leg {
         return this._route;
     }
 
+    set route(route) {
+        this._route = route;
+    }
+
     // try to get a shortened route name, suitable for overview rendering
     get compactRoute() {
         if (this._compactRoute)
@@ -464,6 +531,10 @@ var Leg = class Leg {
         return this._routeType;
     }
 
+    set routeType(routeType) {
+        this._routeType = routeType;
+    }
+
     get departure() {
         return this._departure;
     }
@@ -488,6 +559,10 @@ var Leg = class Leg {
         return this._polyline;
     }
 
+    set polyline(polyline) {
+        this._polyline = polyline;
+    }
+
     get fromCoordinate() {
         return this._fromCoordinate;
     }
@@ -508,6 +583,10 @@ var Leg = class Leg {
         return this._intermediateStops;
     }
 
+    set intermediateStops(intermediateStops) {
+        this._intermediateStops = intermediateStops;
+    }
+
     get headsign() {
         return this._headsign;
     }
@@ -520,6 +599,10 @@ var Leg = class Leg {
         return this._distance;
     }
 
+    set distance(distance) {
+        this._distance = distance;
+    }
+
     get duration() {
         return this._duration;
     }
@@ -544,10 +627,18 @@ var Leg = class Leg {
         return this._color || DEFAULT_ROUTE_COLOR;
     }
 
+    set color(color) {
+        this._color = color;
+    }
+
     get textColor() {
         return this._textColor || DEFAULT_ROUTE_TEXT_COLOR;
     }
 
+    set textColor(textColor) {
+        this._textColor = textColor;
+    }
+
     get tripShortName() {
         return this._tripShortName;
     }
@@ -617,6 +708,10 @@ var Leg = class Leg {
                         case HVT.TAXI_SERVICE:
                             /* TODO: should we have a dedicated taxi icon? */
                             return 'route-car-symbolic';
+
+                        case HVT.AIR_SERVICE:
+                            return 'route-transit-airplane-symbolic';
+
                         default:
                             /* use a fallback question mark icon in case of some future,
                              * for now unknown mode appears */
@@ -632,6 +727,10 @@ var Leg = class Leg {
         return this._walkingInstructions;
     }
 
+    set walkingInstructions(walkingInstructions) {
+        this._walkingInstructions = walkingInstructions;
+    }
+
     /* Pretty print timing for a transit leg, set params.isStart: true when
      * printing for the starting leg of an itinerary.
      * For starting walking legs, only the departure time will be printed,
@@ -713,9 +812,25 @@ var Stop = class Stop {
 };
 
 function sortItinerariesByDepartureAsc(first, second) {
-    return first.departure > second.departure;
+    /* always sort walk-only itineraries first, as they would always be
+     * starting at the earliest possible departure time
+     */
+    if (first.isWalkingOnly)
+        return -1;
+    else if (second.isWalkingOnly)
+        return 1;
+    else
+        return first.departure > second.departure;
 }
 
 function sortItinerariesByArrivalDesc(first, second) {
-    return first.arrival < second.arrival;
-}
\ No newline at end of file
+    /* always sort walk-only itineraries first, as they would always be
+     * ending at the latest possible arrival time
+     */
+    if (first.isWalkingOnly)
+        return -1;
+    else if (second.isWalkingOnly)
+        return 1;
+    else
+        return first.arrival < second.arrival;
+}
diff --git a/src/transitRouter.js b/src/transitRouter.js
new file mode 100644
index 0000000..79fe56d
--- /dev/null
+++ b/src/transitRouter.js
@@ -0,0 +1,255 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2019 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const Champlain = imports.gi.Champlain;
+
+const Service = imports.service;
+const TransitPlan = imports.transitPlan;
+const Utils = imports.utils;
+
+/**
+ * Class responsible for delegating requests to perform routing in transit
+ * mode.
+ * Holds the the shared plan instance (filled with journeys on successful
+ * requests).
+ */
+var TransitRouter = class TransitRoute {
+    constructor(params) {
+        this._plan = new TransitPlan.Plan();
+        this._query = params.query;
+        this._providers = Service.getService().transitProviders;
+        this._providerCache = [];
+        this._language = Utils.getLanguage();
+        this._probePlugins();
+    }
+
+    get enabled() {
+        return this._providers !== undefined;
+    }
+
+    get plan() {
+        return this._plan;
+    }
+
+    /**
+     * Called when the query has been updated to trigger the first set
+     * of results-
+     */
+    fetchFirstResults() {
+        let bestProvider = this._getBestProviderForQuery();
+
+        if (bestProvider) {
+            let provider = bestProvider[0];
+
+            this._currPluginInstance = bestProvider[1];
+            this._plan.attribution = this._getAttributionForProvider(provider);
+            if (provider.attributionUrl)
+                this._plan.attributionUrl = provider.attributionUrl;
+            this._currPluginInstance.fetchFirstResults();
+        } else {
+            this._plan.reset();
+            this._query.reset();
+            this._plan.noProvider();
+        }
+    }
+
+    /**
+     * Called to fetch additional (later or earlier) results depending on the
+     * query settings.
+     */
+    fetchMoreResults() {
+        if (this._currPluginInstance)
+            this._currPluginInstance.fetchMoreResults();
+        else
+            throw new Error('No previous provider');
+    }
+
+    _probePlugins() {
+        this._availablePlugins = [];
+
+        for (let module in imports.transitplugins) {
+            for (let pluginClass in imports.transitplugins[module]) {
+                this._availablePlugins[pluginClass] = module;
+            }
+        }
+    }
+
+    /**
+     * Get attribution for a provider. Returns a language-specific
+     * 'attribution:<lang>' tag if available, otherwise 'attribution'
+     */
+    _getAttributionForProvider(provider) {
+        if (provider['attribution:' + this._language])
+            return provider['attribution:' + this._language];
+        else if (provider.attribution)
+            return provider.attribution;
+        else
+            return null;
+    }
+
+    _getMatchingProvidersForLocation(location) {
+        let country = Utils.getCountryCodeForCoordinates(location.latitude,
+                                                         location.longitude);
+
+        let matchingProviders = [];
+
+        this._providers.forEach((p) => {
+            let provider = p.provider;
+            let areas = provider.areas;
+
+            if (!areas) {
+                Utils.debug('No coverage info for provider ' + provider.name);
+                return;
+            }
+
+            areas.forEach((area) => {
+                /* if the area has a specified priority, override the
+                 * overall area priority, this allows sub-areas of of
+                 * coverage for a provider to have higher or lowe priorities
+                 * than other providers (e.g. one "native" to that area
+                 */
+                if (area.priority)
+                    provider.priority = area.priority;
+
+                let countries = area.countries;
+
+                if (countries) {
+                    if (countries.includes(country)) {
+                        matchingProviders[provider.name] = provider;
+                        return;
+                    }
+                }
+
+                let bbox = area.bbox;
+
+                if (bbox) {
+                    if (bbox.length !== 4) {
+                        Utils.debug('malformed bounding box for provider ' + provider.name);
+                        return;
+                    }
+
+                    let [x1, y1, x2, y2] = bbox;
+                    let cbbox = new Champlain.BoundingBox({ bottom: x1,
+                                                            left: y1,
+                                                            top: x2,
+                                                            right: y2 });
+
+                    if (cbbox.covers(location.latitude,
+                                     location.longitude)) {
+                        matchingProviders[provider.name] = provider;
+                        return;
+                    }
+                }
+            });
+        });
+
+        return matchingProviders;
+    }
+
+    /**
+     * Get the most preferred provider for a given query.
+     * Return: an array with the provider definition and the plugin instance,
+     *         or null if no matching provider was found.
+     */
+    _getBestProviderForQuery() {
+        let startLocation = this._query.filledPoints[0].place.location;
+        let endLocation =
+            this._query.filledPoints.last().place.location;
+
+        let matchingProvidersForStart =
+            this._getMatchingProvidersForLocation(startLocation);
+        let matchingProvidersForEnd =
+            this._getMatchingProvidersForLocation(endLocation);
+
+        let matchingProviders = [];
+
+        // check all candidate providers matching on the start location
+        for (let name in matchingProvidersForStart) {
+            let providerAtStart = matchingProvidersForStart[name];
+            let providerAtEnd = matchingProvidersForEnd[name];
+
+            /* if the provider also matches on the end location, consider it
+             * as a potential candidate
+             */
+            if (providerAtEnd) {
+                let order = this._sortProviders(providerAtStart, providerAtEnd);
+
+                /* add the provider at it lowest priority to favor higher
+                 * priority providers in "fringe cases"
+                 */
+                if (order < 0)
+                    matchingProviders.push(providerAtStart);
+                else
+                    matchingProviders.push(providerAtEnd);
+            }
+        }
+
+        matchingProviders.sort(this._sortProviders);
+
+        for (let i = 0; i < matchingProviders.length; i++) {
+            let provider = matchingProviders[i];
+            let plugin = provider.plugin;
+
+            if (this._providerCache[provider.name])
+                return [provider, this._providerCache[provider.name]];
+
+            let module = this._availablePlugins[plugin];
+
+            if (module) {
+                try {
+                    let params = provider.params;
+                    let instance =
+                        params ? new imports.transitplugins[module][plugin](params) :
+                                 new imports.transitplugins[module][plugin]();
+
+                    this._providerCache[provider.name] = instance;
+
+                    return [provider, instance];
+                } catch (e) {
+                    Utils.debug('Failed to instanciate transit plugin: ' +
+                                plugin + ": " + e);
+                }
+            } else {
+                Utils.debug('Transit provider plugin not available: ' + plugin);
+            }
+        }
+
+        Utils.debug('No suitable transit provider found');
+        return null;
+    }
+
+    /**
+     * Sort function to sort providers in by preference.
+     * If both providers have a priority set, prefers the one with a lower
+     * value (higher prio), otherwise the one that has a priority set (if any),
+     * else no specific order.
+     */
+    _sortProviders(p1, p2) {
+        if (p1.priority && p2.priority)
+            return p1.priority - p2.priority;
+        else if (p1.priority)
+            return -1;
+        else if (p2.priority)
+            return 1;
+        else
+            return 0;
+    }
+};
diff --git a/src/transitTweaks.js b/src/transitTweaks.js
new file mode 100644
index 0000000..578c517
--- /dev/null
+++ b/src/transitTweaks.js
@@ -0,0 +1,178 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2019 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const Champlain = imports.gi.Champlain;
+const GLib = imports.gi.GLib;
+const Soup = imports.gi.Soup;
+
+const Utils = imports.utils;
+
+const BASE_URL = 'https://gis.gnome.org/services/aux';
+
+var TransitTweaks = class {
+    constructor(params) {
+        this._name = params.name;
+        this._session =
+            new Soup.Session({ user_agent: 'gnome-maps/' + pkg.version });
+
+        if (!this._name)
+            throw new Error('Missing tweak name');
+    }
+
+    applyTweaks(itineraries, callback) {
+        if (!this._tweaks) {
+            let variable = 'TRANSIT_TWEAKS_' + this._name.toUpperCase();
+            let filename = GLib.getenv(variable);
+
+            if (filename) {
+                this._readTweaksFromFile(filename);
+                this._doApplyTweaks(itineraries, callback);
+            } else {
+                this._fetchTweaksAsync(itineraries, callback);
+            }
+        } else {
+            this._doApplyTweaks(itineraries, callback);
+        }
+    }
+
+    _doApplyTweaks(itineraries, callback) {
+        if (this._tweaks !== {}) {
+            itineraries.forEach((itinerary) =>
+                this._applyTweaksToItinerary(itinerary));
+        }
+
+        callback();
+    }
+
+    _readTweaksFromFile(filename) {
+        let data = Utils.readFile(filename);
+
+        if (!data) {
+            Utils.debug('Failed to read from tweak file');
+            callback();
+        }
+
+        try {
+            this._tweaks = JSON.parse(Utils.getBufferText(data));
+        } catch (e) {
+            Utils.debug('Failed to parse tweaks: ' + e);
+            this._tweaks = {};
+        }
+    }
+
+    _fetchTweaksAsync(itineraries, callback) {
+        let uri = new Soup.URI(BASE_URL + '/' + 'tweaks-' + this._name + '.json');
+        let request = new Soup.Message({ method: 'GET', uri: uri });
+
+        this._session.queue_message(request, (obj, message) => {
+            if (message.status_code !== Soup.Status.OK) {
+                Utils.debug('Failed to download tweaks');
+                callback();
+            } else {
+                try {
+                    this._tweaks = JSON.parse(message.response_body.data);
+                } catch (e) {
+                    Utils.debug('Failed to parse tweaks: ' + e);
+                    this._tweaks = {};
+                }
+
+                this._doApplyTweaks(itineraries, callback);
+            }
+        });
+    }
+
+    _applyTweaksToItinerary(itinerary) {
+        itinerary.legs.forEach((leg) => {
+            if (leg.transit)
+                this._applyTweaksToLeg(leg);
+        });
+    }
+
+    _applyTweaksToLeg(leg) {
+        let agencyTweaks = this._tweaks.agencies[leg.agencyName];
+
+        if (agencyTweaks) {
+            let routeTypeTweaks = agencyTweaks.routeTypes[leg.routeType];
+
+            if (routeTypeTweaks) {
+                let tweakToApply;
+                let bboxTweaks = routeTypeTweaks.bboxes;
+                let routeTweaks = routeTypeTweaks.routes ?
+                                  routeTypeTweaks.routes[leg.route] || null : null;
+                let routePatternTweaks = routeTypeTweaks.routePatterns;
+
+                // first check for boundingbox-specific tweaks
+                if (bboxTweaks) {
+                    bboxTweaks.forEach((tweak) => {
+                        let bbox = tweak.bbox;
+                        let cbbox = new Champlain.BoundingBox({ bottom: bbox[0],
+                                                                left: bbox[1],
+                                                                top: bbox[2],
+                                                                right: bbox[3] });
+
+                        if (cbbox.covers(leg.polyline[0].latitude,
+                                         leg.polyline[0].longitude)) {
+                            /* if boundingbox fits, use embedded route or
+                             * route pattern tweaks
+                             */
+                            routeTweaks = tweak.routes ?
+                                          tweak.routes[leg.route] : null;
+                            routePatternTweaks = tweak.routePatterns;
+                        }
+                    });
+                }
+
+                if (routeTweaks) {
+                    tweakToApply = routeTweaks;
+                } else if (routePatternTweaks) {
+                    routePatternTweaks.forEach((pattern) => {
+                        if (!(pattern.regex instanceof RegExp)) {
+                            pattern.regex = new RegExp(pattern.regex);
+                        }
+
+                        if (leg.route.match(pattern.regex))
+                            tweakToApply = pattern;
+                    });
+                }
+
+                if (!tweakToApply) {
+                    tweakToApply = routeTypeTweaks;
+                }
+
+                this._applyRouteTweaksToLeg(leg, tweakToApply);
+            }
+        }
+    }
+
+    _applyRouteTweaksToLeg(leg, tweaks) {
+        if (tweaks.route)
+            leg.route = tweaks.route;
+
+        if (tweaks.routeType)
+            leg.routeType = tweaks.routeType;
+
+        if (tweaks.color)
+            leg.color = tweaks.color;
+
+        if (tweaks.textColor)
+            leg.textColor = tweaks.textColor;
+    }
+}
diff --git a/src/transitplugins/README b/src/transitplugins/README
new file mode 100644
index 0000000..890401c
--- /dev/null
+++ b/src/transitplugins/README
@@ -0,0 +1,67 @@
+This directory contains implementations of transit routing provider plugins.
+Each plugin should contain an ES6 class implementing the plugin.
+
+Each implementation implements two methods:
+
+fetchFirstResults():
+
+This is invoked when the singleton routing query has been updated and would
+query itineraries from it's source, and on success populate the TransitPlan
+singleton with an itinerary list and call plan.update(), or on error call
+one of the pre-defined error methods on the plan, or trigger a custom error
+with plan.error().
+
+fetchMoreResults():
+
+This is invoked when to fetch additional (later or earlier) results.
+Would on success add additional itineraries and call plan.update(), or on
+error call plan.noMoreResults().
+
+Providers are configured via the downloaded service file using a JSON element
+like:
+
+"transitProviders": [
+    {
+        "provider": {
+            "name": "Description of provider 1",
+            "plugin": "OpenTripPlanner",
+            "attribution": "Provider 1",
+            "attributionUrl": "http://provider1.example.com",
+            "priority": 10,
+            "areas": [
+                {
+                    "priority": 10,
+                    "countries": [ "UT" ]
+                }
+            ],
+            "params": {
+               "baseUrl": "http://otp.provider1.example.com/otp"
+            }
+        }
+    },
+    {
+        "provider": {
+            "name": "Provider 2",
+            "plugin": "OpenTripPlanner",
+            "attribution": "Provider 2",
+            "attributionUrl": "https://provider2.example.com",
+            "areas": [
+                {
+                    "bbox": [48.28,0.81,49.73,4.11]
+                }
+            ],
+            "params": {
+                "baseUrl": "https://provider2.example.com/otp"
+            }
+        }
+    }
+ ]
+
+ Each provider can have an optional priority, to allow more specific provider
+ (e.g. one serving a single city) within the area of a more general one.
+ Single sub-areas of a provider can also override the general provider priority.
+ This can be used to allow areas of provider to "shadow" neighboring providers
+ while keeping the the neighboring provider as the preferred one when used
+ exclusively for its region.
+ Custom parameters, if specified, will be passed as the "params" object the
+ constructor() of the plugin implementation.
\ No newline at end of file
diff --git a/src/openTripPlanner.js b/src/transitplugins/openTripPlanner.js
similarity index 63%
rename from src/openTripPlanner.js
rename to src/transitplugins/openTripPlanner.js
index b909df7..c3fcbc1 100644
--- a/src/openTripPlanner.js
+++ b/src/transitplugins/openTripPlanner.js
@@ -19,14 +19,20 @@
  * Author: Marcus Lundblad <ml@update.uu.se>
  */
 
+const _ = imports.gettext.gettext;
+
 const Champlain = imports.gi.Champlain;
 const GLib = imports.gi.GLib;
 const Soup = imports.gi.Soup;
 
+const Application = imports.application;
 const EPAF = imports.epaf;
+const GraphHopperTransit = imports.graphHopperTransit;
 const HTTP = imports.http;
+const HVT = imports.hvt;
 const Location = imports.location;
 const Place = imports.place;
+const Route = imports.route;
 const RouteQuery = imports.routeQuery;
 const Service = imports.service;
 const TransitPlan = imports.transitPlan;
@@ -35,27 +41,17 @@ const Utils = imports.utils;
 /**
  * This module implements the interface for communicating with an OpenTripPlanner
  * server instance.
- * The code is somwhat intricate, because it assumes running OpenTripPlanner with
- * only transit data and relies on calling out to GraphHopper to do turn-by-turn-
- * based routing for the walking portions, thus it's based on an asynchronous
- * recursive pattern. The reason for running OpenTripPlanner with only transit
- * data is that prior experiments has shown that OpenTripPlanner with full OSM
- * data doesn't scale well beyong single cities, and GraphHopper has already
- * given us good results before.
+ * The code is somewhat intricate since it supports instances of OpenTripPlanner
+ * running both with and without OSM data to complement the transit timetable
+ * data with turn-by-turn (walking) routing.
  *
  * There is two entry points for obtaining routes, one which is called by the
  * routing delegator when the query is being modified (fetchFirstResults()),
  * and the other being called when requesting additional results (later or
  * earlier alternatives depending on search criteria) (fetchMoreResults())
- * These call into an entry point function "_fatchRoute()" which first calls
- * out to the function "_fetchRouters()" which calls out to the server to update
- * the cached router list if needed (routers are the OpenTripPlanner terminology
- * for an isolated graph, routing can not occur between graphs).
- * In the callback from _fetchRouters, an array of suitable routers (covering
- * start and end coordinates for the desired route) is processed.
- * "_fetchRoutes()" is called, which will do asynchronous recursive call for
- * each router obtained earlier.
- * "_fetchRoutesForRouter()" is called on each router, which in turn
+ * These call into an entry point function "_fatchRoute()".
+ * "_fetchRoutes()" is called.
+ * In the case where there is no OSM data (onlyTransitData is true), it
  * asyncronously calls "_fetchTransitStops()" to get closest transit stop for
  * each of the query point, this function will involve OpenTripPlanner calls to
  * find stops within a search circle around the coordinate and then calls out
@@ -76,11 +72,6 @@ const Utils = imports.utils;
  * API docs for OpenTripPlanner can be found at: http://dev.opentripplanner.org/apidoc/1.0.0/
  */
 
-/* timeout after which the routers data is considered stale and we will force
- * a reload (24 hours)
- */
-const ROUTERS_TIMEOUT = 24 * 60 * 60 * 1000;
-
 /* minimum distance when an explicit walk route will be requested to suppliment
  * the transit route
  */
@@ -122,17 +113,21 @@ const GAP_BEFORE_MORE_RESULTS = 120;
 var OpenTripPlanner = class OpenTripPlanner {
 
     constructor(params) {
-        this._session = new Soup.Session();
-        /* initially set routers as updated far back in the past to force
-         * a download when first request
-         */
-        this._routersUpdatedTimestamp = 0;
-        this._query = params.query;
-        this._graphHopper = params.graphHopper;
-        this._plan = new TransitPlan.Plan();
-        this._baseUrl = this._getBaseUrl();
-        this._walkingRoutes = [];
+        this._session = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version });
+        this._plan = Application.routingDelegator.transitRouter.plan;
+        this._query = Application.routeQuery;
+        this._baseUrl = params.baseUrl;
+        this._router = params.router || 'default';
+        this._routerUrl = params.routerUrl || null;
+        this._onlyTransitData = params.onlyTransitData || false;
         this._extendPrevious = false;
+        this._language = Utils.getLanguage();
+
+        if (!this._baseUrl && !this._routerUrl)
+            throw new Error('must specify either baseUrl or routerUrl as an argument');
+
+        if (this._baseUrl && this._routerUrl)
+            throw new Error('can not specify both baseUrl and routerUrl as arguments');
     }
 
     get plan() {
@@ -153,86 +148,9 @@ var OpenTripPlanner = class OpenTripPlanner {
         this._fetchRoute();
     }
 
-    _getBaseUrl() {
-        let debugUrl = GLib.getenv('OTP_BASE_URL');
-
-        if (debugUrl) {
-            return debugUrl;
-        } else {
-            let otp = Service.getService().openTripPlanner
-
-            if (otp && otp.baseUrl) {
-                return otp.baseUrl;
-            } else {
-                Utils.debug('No OpenTripPlanner URL defined in service file');
-                return null;
-            }
-        }
-    }
-
-    _getRouterUrl(router) {
-        if (!router || router.length === 0)
-            router = 'default';
-
-        return this._baseUrl + '/routers/' + router;
-    }
-
-    _fetchRouters(callback) {
-        let currentTime = (new Date()).getTime();
-
-        if (currentTime - this._routersUpdatedTimestamp < ROUTERS_TIMEOUT) {
-            callback(true);
-        } else {
-            let uri = new Soup.URI(this._baseUrl + '/routers');
-            let request = new Soup.Message({ method: 'GET', uri: uri });
-
-            request.request_headers.append('Accept', 'application/json');
-            this._session.queue_message(request, (obj, message) => {
-                if (message.status_code !== Soup.Status.OK) {
-                    callback(false);
-                    return;
-                }
-
-                try {
-                    this._routers = JSON.parse(message.response_body.data);
-                    this._routersUpdatedTimestamp = (new Date()).getTime();
-                    callback(true);
-                } catch (e) {
-                    Utils.debug('Failed to parse router information');
-                    callback(false);
-                }
-            });
-        }
-    }
-
-    _getRoutersForPlace(place) {
-        let routers = [];
-
-        this._routers.routerInfo.forEach((routerInfo) => {
-            /* TODO: only check bounding rectangle for now
-             * should we try to do a finer-grained check using the bounding
-             * polygon (if OTP gives one for the routers).
-             * And should we add some margins to allow routing from just outside
-             * a network (walking distance)?
-             */
-            if (place.location.latitude >= routerInfo.lowerLeftLatitude &&
-                place.location.latitude <= routerInfo.upperRightLatitude &&
-                place.location.longitude >= routerInfo.lowerLeftLongitude &&
-                place.location.longitude <= routerInfo.upperRightLongitude)
-                routers.push(routerInfo.routerId);
-        });
-
-        return routers;
-    }
-
-    /* Note: this is theoretically slow (O(n*m)), but we will have filtered
-     * possible routers for the starting and ending query point, so they should
-     * be short (in many cases just one element)
-     */
-    _routerIntersection(routers1, routers2) {
-        return routers1.filter(function(n) {
-            return routers2.indexOf(n) != -1;
-        });
+    _getRouterUrl() {
+        return this._routerUrl ? this._routerUrl :
+                                 this._baseUrl + '/routers/' + this._router;
     }
 
     _getMode(routeType) {
@@ -247,6 +165,8 @@ var OpenTripPlanner = class OpenTripPlanner {
             return 'BUS';
         case TransitPlan.RouteType.FERRY:
             return 'FERRY';
+        case HVT.AIR_SERVICE:
+            return 'AIRPLANE';
         default:
             throw new Error('unhandled route type');
         }
@@ -257,6 +177,11 @@ var OpenTripPlanner = class OpenTripPlanner {
             return this._getMode(transitType);
         });
 
+        /* should always include walk when setting explicit modes,
+         * otherwise only routes ending close to a stop would work
+         */
+        modes.push('WALK');
+
         return modes.join(',');
     }
 
@@ -265,7 +190,7 @@ var OpenTripPlanner = class OpenTripPlanner {
             let points = this._query.filledPoints;
             let stop = stops[index];
             let stopPoint =
-                this._createQueryPointForCoord([stop.lat, stop.lon]);
+                GraphHopperTransit.createQueryPointForCoord([stop.lat, stop.lon]);
 
             if (stops[0].dist < 100) {
                 /* if the stop is close enough to the intended point, just
@@ -273,7 +198,7 @@ var OpenTripPlanner = class OpenTripPlanner {
                 this._selectBestStopRecursive(stops, index + 1, stopIndex,
                                               callback);
             } else if (stopIndex === 0) {
-                this._fetchWalkingRoute([points[0], stopPoint],
+                GraphHopperTransit.fetchWalkingRoute([points[0], stopPoint],
                                         (route) => {
                     /* if we couldn't find an exact walking route, go with the
                      * "as the crow flies" distance */
@@ -283,7 +208,8 @@ var OpenTripPlanner = class OpenTripPlanner {
                                                   callback);
                 });
             } else if (stopIndex === points.length - 1) {
-                this._fetchWalkingRoute([stopPoint, points.last()], (route) => {
+                GraphHopperTransit.fetchWalkingRoute([stopPoint, points.last()],
+                                                     (route) => {
                     if (route)
                         stop.dist = route.distance;
                     this._selectBestStopRecursive(stops, index + 1, stopIndex,
@@ -316,10 +242,10 @@ var OpenTripPlanner = class OpenTripPlanner {
         return s1.dist > s2.dist;
     }
 
-    _fetchRoutesForStop(router, stop, callback) {
+    _fetchRoutesForStop(stop, callback) {
         let query = new HTTP.Query();
-        let uri = new Soup.URI(this._getRouterUrl(router) +
-                               '/index/stops/' + stop.id + '/routes');
+        let uri = new Soup.URI(this._getRouterUrl() + '/index/stops/' +
+                               stop.id + '/routes');
         let request = new Soup.Message({ method: 'GET', uri: uri });
 
         request.request_headers.append('Accept', 'application/json');
@@ -358,11 +284,11 @@ var OpenTripPlanner = class OpenTripPlanner {
         return false;
     }
 
-    _filterStopsRecursive(router, stops, index, filteredStops, callback) {
+    _filterStopsRecursive(stops, index, filteredStops, callback) {
         if (index < stops.length) {
             let stop = stops[index];
 
-            this._fetchRoutesForStop(router, stop, (routes) => {
+            this._fetchRoutesForStop(stop, (routes) => {
                 for (let i = 0; i < routes.length; i++) {
                     let route = routes[i];
 
@@ -371,19 +297,19 @@ var OpenTripPlanner = class OpenTripPlanner {
                         break;
                     }
                 }
-                this._filterStopsRecursive(router, stops, index + 1,
-                                           filteredStops, callback);
+                this._filterStopsRecursive(stops, index + 1, filteredStops,
+                                           callback);
             });
         } else {
             callback(filteredStops);
         }
     }
 
-    _filterStops(router, stops, callback) {
-        this._filterStopsRecursive(router, stops, 0, [], callback);
+    _filterStops(stops, callback) {
+        this._filterStopsRecursive(stops, 0, [], callback);
     }
 
-    _fetchTransitStopsRecursive(router, index, result, callback) {
+    _fetchTransitStopsRecursive(index, result, callback) {
         let points = this._query.filledPoints;
 
         if (index < points.length) {
@@ -392,7 +318,7 @@ var OpenTripPlanner = class OpenTripPlanner {
                            lon: point.place.location.longitude,
                            radius: STOP_SEARCH_RADIUS };
             let query = new HTTP.Query(params);
-            let uri = new Soup.URI(this._getRouterUrl(router) +
+            let uri = new Soup.URI(this._getRouterUrl() +
                                    '/index/stops?' + query.toString());
             let request = new Soup.Message({ method: 'GET', uri: uri });
 
@@ -417,11 +343,11 @@ var OpenTripPlanner = class OpenTripPlanner {
                         Utils.debug('stops: ' + JSON.stringify(stops, '', 2));
                         this._selectBestStop(stops, index, (stop) => {
                             result.push(stop);
-                            this._fetchTransitStopsRecursive(router, index + 1,
-                                                             result, callback);
+                            this._fetchTransitStopsRecursive(index + 1, result,
+                                                             callback);
                         });
                     } else {
-                        this._filterStops(router, stops, (filteredStops) => {
+                        this._filterStops(stops, (filteredStops) => {
                             filteredStops.sort(this._sortTransitStops);
                             filteredStops = filteredStops.splice(0, NUM_STOPS_TO_TRY);
 
@@ -433,7 +359,7 @@ var OpenTripPlanner = class OpenTripPlanner {
 
                             this._selectBestStop(filteredStops, index, (stop) => {
                                 result.push(stop);
-                                this._fetchTransitStopsRecursive(router, index + 1,
+                                this._fetchTransitStopsRecursive(index + 1,
                                                                  result, callback);
                             });
                         });
@@ -445,8 +371,8 @@ var OpenTripPlanner = class OpenTripPlanner {
         }
     }
 
-    _fetchTransitStops(router, callback) {
-        this._fetchTransitStopsRecursive(router, 0, [], callback);
+    _fetchTransitStops(callback) {
+        this._fetchTransitStopsRecursive(0, [], callback);
     }
 
     // get a time suitably formatted for the OpenTripPlanner query param
@@ -465,25 +391,14 @@ var OpenTripPlanner = class OpenTripPlanner {
         return date.format('%F');
     }
 
-    // create parameter map for the request, given query and options
-    _createParams(stops) {
-        let params = { fromPlace: stops[0].id,
-                       toPlace: stops.last().id };
-        let intermediatePlaces = [];
-
-        for (let i = 1; i < stops.length - 1; i++) {
-            intermediatePlaces.push(stops[i].id);
-        }
-        if (intermediatePlaces.length > 0)
-            params.intermediatePlaces = intermediatePlaces;
+    _getPlaceParamFromLocation(location) {
+        return location.latitude + ',' + location.longitude;
+    }
 
+    _addCommonParams(params) {
         params.numItineraries = 5;
         params.showIntermediateStops = true;
-        /* set walking speed for transfers to a slightly lower value to
-         * compensate for running OTP with only transit data, giving straight-
-         * line walking paths
-         */
-        params.walkSpeed = 1.0;
+        params.locale = this._language;
 
         let time = this._query.time;
         let date = this._query.date;
@@ -529,68 +444,106 @@ var OpenTripPlanner = class OpenTripPlanner {
         let options = this._query.transitOptions;
         if (options && !options.showAllTransitTypes)
             params.mode = this._getModes(options);
+    }
+
+    _createParamsWithLocations() {
+        let points = this._query.filledPoints;
+        let params = {
+            fromPlace: this._getPlaceParamFromLocation(points[0].place.location),
+            toPlace: this._getPlaceParamFromLocation(points[points.length - 1].place.location) };
+        let intermediatePlaces = [];
+
+        for (let i = 1; i < points.length - 1; i++) {
+            let location = points[i].place.location;
+            intermediatePlaces.push(this._getPlaceParamFromLocation(location));
+        }
+        if (intermediatePlaces)
+            params.intermediatePlaces = intermediatePlaces;
+
+        params.maxWalkDistance = 2500;
+        this._addCommonParams(params);
 
         return params;
     }
 
-    _fetchRoutesForRouter(router, callback) {
-        this._fetchTransitStops(router, (stops) => {
-            let points = this._query.filledPoints;
+    // create parameter map for the request, given query and options
+    _createParamsWithStops(stops) {
+        let params = { fromPlace: stops[0].id,
+                       toPlace: stops.last().id };
+        let intermediatePlaces = [];
 
-            if (!stops) {
-                callback(null);
-                return;
-            }
+        for (let i = 1; i < stops.length - 1; i++) {
+            intermediatePlaces.push(stops[i].id);
+        }
+        if (intermediatePlaces.length > 0)
+            params.intermediatePlaces = intermediatePlaces;
 
-            /* if there's only a start and end stop (no intermediate stops)
-             * and those stops are identical, reject the routing, since this
-             * means there would be no point in transit, and OTP would give
-             * some bizarre option like boarding transit, go one stop and then
-             * transfer to go back the same route
-             */
-            if (stops.length === 2 && stops[0].id === stops[1].id) {
-                callback(null);
-                return;
-            }
+        /* set walking speed for transfers to a slightly lower value to
+         * compensate for running OTP with only transit data, giving straight-
+         * line walking paths
+         */
+        params.walkSpeed = 1.0;
 
-            let params = this._createParams(stops);
-            let query = new HTTP.Query(params);
-            let uri = new Soup.URI(this._getRouterUrl(router) + '/plan?' +
-                                   query.toString());
-            let request = new Soup.Message({ method: 'GET', uri: uri });
+        this._addCommonParams(params);
 
-            request.request_headers.append('Accept', 'application/json');
-            this._session.queue_message(request, (obj, message) => {
-                if (message.status_code !== Soup.Status.OK) {
-                    Utils.debug('Failed to get route plan from router ' +
-                                routers[index] + ' ' + message);
+        return params;
+    }
+
+    _fetchPlan(params, callback) {
+        let query = new HTTP.Query(params);
+        let uri = new Soup.URI(this._getRouterUrl() + '/plan?' +
+                               query.toString());
+        let request = new Soup.Message({ method: 'GET', uri: uri });
+
+        request.request_headers.append('Accept', 'application/json');
+        this._session.queue_message(request, (obj, message) => {
+            if (message.status_code !== Soup.Status.OK) {
+                Utils.debug('Failed to get route plan from router ' +
+                            this._router + ' ' + message);
+                callback(null);
+            } else {
+                try {
+                    let result = JSON.parse(message.response_body.data);
+
+                    callback(result);
+                } catch (e) {
+                    Utils.debug('Error parsing result: ' + e);
                     callback(null);
-                } else {
-                    callback(JSON.parse(message.response_body.data));
                 }
-            });
+            }
         });
     }
 
-    _fetchRoutesRecursive(routers, index, result, callback) {
-        if (index < routers.length) {
-            let router = routers[index];
+    _fetchRoutes(callback) {
+        if (this._onlyTransitData) {
+            this._fetchTransitStops((stops) => {
+                let points = this._query.filledPoints;
 
-            this._fetchRoutesForRouter(router, (response) => {
-                if (response) {
-                    Utils.debug('plan: ' + JSON.stringify(response, '', 2));
-                    result.push(response);
+                if (!stops) {
+                    callback(null);
+                    return;
+                }
+
+                /* if there's only a start and end stop (no intermediate stops)
+                 * and those stops are identical, reject the routing, since this
+                 * means there would be no point in transit, and OTP would give
+                 * some bizarre option like boarding transit, go one stop and then
+                 * transfer to go back the same route
+                 */
+                if (stops.length === 2 && stops[0].id === stops[1].id) {
+                    callback(null);
+                    return;
                 }
 
-                this._fetchRoutesRecursive(routers, index + 1, result, callback);
+                let params = this._createParamsWithStops(stops);
+
+                this._fetchPlan(params, callback);
             });
         } else {
-            callback(result);
-        }
-    }
+            let params = this._createParamsWithLocations();
 
-    _fetchRoutes(routers, callback) {
-        this._fetchRoutesRecursive(routers, 0, [], callback);
+            this._fetchPlan(params, callback);
+        }
     }
 
     _reset() {
@@ -612,44 +565,39 @@ var OpenTripPlanner = class OpenTripPlanner {
             this.plan.noMoreResults();
         } else {
             this._reset();
-            this.plan.error(_("No route found."));
+            this.plan.noRouteFound();
         }
     }
 
     _fetchRoute() {
-        this._fetchRouters((success) => {
-            if (success) {
-                let points = this._query.filledPoints;
-                let routers = this._getRoutersForPoints(points);
-
-                if (routers.length > 0) {
-                    this._fetchRoutes(routers, (routes) => {
-                        let itineraries = [];
-                        routes.forEach((plan) => {
-                            if (plan.plan && plan.plan.itineraries) {
-                                itineraries =
-                                    itineraries.concat(
-                                        this._createItineraries(plan.plan.itineraries));
-                            }
-                        });
+        let points = this._query.filledPoints;
 
-                        if (itineraries.length === 0) {
-                            /* don't reset query points, unlike for turn-based
-                             * routing, since options and timeing might influence
-                             * results */
-                            this._noRouteFound();
-                        } else {
-                            this._recalculateItineraries(itineraries);
-                        }
-                    });
+        this._fetchRoutes((route) => {
+            if (route) {
+                let itineraries = [];
+                let plan = route.plan;
+
+                Utils.debug('route: ' + JSON.stringify(route, null, 2));
+
+                if (plan && plan.itineraries) {
+                    itineraries =
+                        itineraries.concat(
+                            this._createItineraries(plan.itineraries));
+                }
 
+                if (itineraries.length === 0) {
+                    /* don't reset query points, unlike for turn-based
+                     * routing, since options and timeing might influence
+                     * results */
+                    this._noRouteFound();
                 } else {
-                    this._reset();
-                    this.plan.error(_("No timetable data found for this route."));
+                    if (this._onlyTransitData)
+                        this._recalculateItineraries(itineraries);
+                    else
+                        this._updateWithNewItineraries(itineraries);
                 }
             } else {
-                this._reset();
-                this.plan.error(_("Route request failed."));
+                this._noRouteFound();
             }
         });
     }
@@ -701,6 +649,17 @@ var OpenTripPlanner = class OpenTripPlanner {
         return true;
     }
 
+    /**
+     * Update plan with new itineraries, setting the new itineraries if it's
+     * the first fetch for a query, or extending the existing ones if it's
+     * a request to load more
+     */
+    _updateWithNewItineraries(itineraries) {
+        this.plan.updateWithNewItineraries(itineraries, this._query.arriveBy,
+                                           this._extendPrevious);
+        this._extendPrevious = false;
+    }
+
     _recalculateItinerariesRecursive(itineraries, index) {
         if (index < itineraries.length) {
             this._recalculateItinerary(itineraries[index], (itinerary) => {
@@ -726,88 +685,13 @@ var OpenTripPlanner = class OpenTripPlanner {
 
             if (filteredItineraries.length > 0) {
                 filteredItineraries.forEach((itinerary) => itinerary.adjustTimings());
-
-                /* sort itineraries, by departure time ascending if querying
-                 * by leaving time, by arrival time descending when querying
-                 * by arriving time
-                 */
-                if (this._query.arriveBy)
-                    filteredItineraries.sort(TransitPlan.sortItinerariesByArrivalDesc);
-                else
-                    filteredItineraries.sort(TransitPlan.sortItinerariesByDepartureAsc);
-
-                let newItineraries = this._extendPrevious ?
-                                     this.plan.itineraries.concat(filteredItineraries) :
-                                     filteredItineraries;
-
-                // reset the "load more results" flag
-                this._extendPrevious = false;
-                this.plan.update(newItineraries);
+                this._updateWithNewItineraries(filteredItineraries);
             } else {
                 this._noRouteFound();
             }
         }
     }
 
-    // create a straight-line "as the crow flies" polyline between two places
-    _createStraightPolyline(fromLoc, toLoc) {
-        return [new Champlain.Coordinate({ latitude: fromLoc.latitude,
-                                           longitude: fromLoc.longitude }),
-                new Champlain.Coordinate({ latitude: toLoc.latitude,
-                                           longitude: toLoc.longitude })];
-    }
-
-    /* Creates a new walking leg given start and end places, and a route
-     * obtained from GraphHopper. If the route is undefined (which happens if
-     * GraphHopper failed to obtain a walking route, approximate it with a
-     * straight line. */
-    _createWalkingLeg(from, to, fromName, toName, route) {
-        let fromLocation = from.place.location;
-        let toLocation = to.place.location;
-        let fromCoordinate = [fromLocation.latitude, fromLocation.longitude];
-        let toCoordinate = [toLocation.latitude, toLocation.longitude];
-        let polyline = route ? route.path :
-                               this._createStraightPolyline(fromLocation, toLocation);
-        let distance = route ? route.distance :
-                               fromLocation.get_distance_from(toLocation) * 1000;
-        /* as an estimate for approximated straight-line walking legs,
-         * assume a speed of 1 m/s to allow some extra time */
-        let duration = route ? route.time / 1000 : distance;
-        let walkingInstructions = route ? route.turnPoints : null;
-
-        return new TransitPlan.Leg({ fromCoordinate: fromCoordinate,
-                                     toCoordinate: toCoordinate,
-                                     from: fromName,
-                                     to: toName,
-                                     isTransit: false,
-                                     polyline: polyline,
-                                     duration: duration,
-                                     distance: distance,
-                                     walkingInstructions: walkingInstructions });
-    }
-
-    /* fetches walking route and stores the route for the given coordinate
-     * pair to avoid requesting the same route over and over from GraphHopper
-     */
-    _fetchWalkingRoute(points, callback) {
-        let index = points[0].place.location.latitude + ',' +
-                    points[0].place.location.longitude + ';' +
-                    points[1].place.location.latitude + ',' +
-                    points[1].place.location.longitude;
-        let route = this._walkingRoutes[index];
-
-        if (!route) {
-            this._graphHopper.fetchRouteAsync(points,
-                                              RouteQuery.Transportation.PEDESTRIAN,
-                                              (newRoute) => {
-                this._walkingRoutes[index] = newRoute;
-                callback(newRoute);
-            });
-        } else {
-            callback(route);
-        }
-    }
-
     _recalculateItinerary(itinerary, callback) {
         let from = this._query.filledPoints[0];
         let to = this._query.filledPoints.last();
@@ -817,9 +701,12 @@ var OpenTripPlanner = class OpenTripPlanner {
              * leg is a non-transit (walking), recalculate the route in its entire
              * using walking
              */
-            this._fetchWalkingRoute(this._query.filledPoints, (route) => {
-                let leg = this._createWalkingLeg(from, to, from.place.name,
-                                                 to.place.name, route);
+            GraphHopperTransit.fetchWalkingRoute(this._query.filledPoints,
+                                                 (route) => {
+                let leg = GraphHopperTransit.createWalkingLeg(from, to,
+                                                              from.place.name,
+                                                              to.place.name,
+                                                              route);
                 let newItinerary =
                     new TransitPlan.Itinerary({departure: itinerary.departure,
                                                duration: route.time / 1000,
@@ -829,8 +716,8 @@ var OpenTripPlanner = class OpenTripPlanner {
         } else if (itinerary.legs.length === 1 && itinerary.legs[0].transit) {
             // special case if there is extactly one transit leg
             let leg = itinerary.legs[0];
-            let startLeg = this._createQueryPointForCoord(leg.fromCoordinate);
-            let endLeg = this._createQueryPointForCoord(leg.toCoordinate);
+            let startLeg = GraphHopperTransit.createQueryPointForCoord(leg.fromCoordinate);
+            let endLeg = GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
             let fromLoc = from.place.location;
             let startLoc = startLeg.place.location;
             let endLoc = endLeg.place.location;
@@ -843,14 +730,19 @@ var OpenTripPlanner = class OpenTripPlanner {
                 /* add an extra walking leg to both the beginning and end of the
                  * itinerary
                  */
-                this._fetchWalkingRoute([from, startLeg], (firstRoute) => {
+                GraphHopperTransit.fetchWalkingRoute([from, startLeg],
+                                                     (firstRoute) => {
                     let firstLeg =
-                        this._createWalkingLeg(from, startLeg, from.place.name,
-                                               leg.from, firstRoute);
-                    this._fetchWalkingRoute([endLeg, to], (lastRoute) => {
-                        let lastLeg = this._createWalkingLeg(endLeg, to, leg.to,
-                                                             to.place.name,
-                                                             lastRoute);
+                        GraphHopperTransit.createWalkingLeg(from, startLeg,
+                                                            from.place.name,
+                                                            leg.from, firstRoute);
+                    GraphHopperTransit.fetchWalkingRoute([endLeg, to],
+                                                         (lastRoute) => {
+                        let lastLeg =
+                            GraphHopperTransit.createWalkingLeg(endLeg, to,
+                                                                leg.to,
+                                                                to.place.name,
+                                                                lastRoute);
                         itinerary.legs.unshift(firstLeg);
                         itinerary.legs.push(lastLeg);
                         callback(itinerary);
@@ -858,10 +750,12 @@ var OpenTripPlanner = class OpenTripPlanner {
                 });
             } else if (endWalkDistance >= MIN_WALK_ROUTING_DISTANCE) {
                 // add an extra walking leg to the end of the itinerary
-                this._fetchWalkingRoute([endLeg, to], (lastRoute) => {
+                GraphHopperTransit.fetchWalkingRoute([endLeg, to],
+                                                     (lastRoute) => {
                     let lastLeg =
-                        this._createWalkingLeg(endLeg, to, leg.to,
-                                               to.place.name, lastRoute);
+                        GraphHopperTransit.createWalkingLeg(endLeg, to, leg.to,
+                                                            to.place.name,
+                                                            lastRoute);
                     itinerary.legs.push(lastLeg);
                     callback(itinerary);
                 });
@@ -882,26 +776,15 @@ var OpenTripPlanner = class OpenTripPlanner {
         }
     }
 
-    _createQueryPointForCoord(coord) {
-        let location = new Location.Location({ latitude: coord[0],
-                                               longitude: coord[1],
-                                               accuracy: 0 });
-        let place = new Place.Place({ location: location });
-        let point = new RouteQuery.QueryPoint();
-
-        point.place = place;
-        return point;
-    }
-
     _recalculateItineraryRecursive(itinerary, index, callback) {
         if (index < itinerary.legs.length) {
             let leg = itinerary.legs[index];
             if (index === 0) {
                 let from = this._query.filledPoints[0];
                 let startLeg =
-                    this._createQueryPointForCoord(leg.fromCoordinate);
+                    GraphHopperTransit.createQueryPointForCoord(leg.fromCoordinate);
                 let endLeg =
-                    this._createQueryPointForCoord(leg.toCoordinate);
+                    GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
                 let fromLoc = from.place.location;
                 let startLegLoc = startLeg.place.location;
                 let endLegLoc = endLeg.place.location;
@@ -927,21 +810,22 @@ var OpenTripPlanner = class OpenTripPlanner {
                      * "pass by" a stop at the next step in the itinerary due to
                      * similar reasons
                      */
-                    let to = this._createQueryPointForCoord(leg.toCoordinate);
+                    let to = GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
                     let toName = leg.to;
 
                     /* if the next leg is a walking one, "fold" it into the one
                      * we create here */
                     if (nextLeg && !nextLeg.transit) {
-                        to = this._createQueryPointForCoord(nextLeg.toCoordinate);
+                        to = GraphHopperTransit.createQueryPointForCoord(nextLeg.toCoordinate);
                         toName = nextLeg.to;
                         itinerary.legs.splice(index + 1, index + 1);
                     }
 
-                    this._fetchWalkingRoute([from, to], (route) => {
+                    GraphHopperTransit.fetchWalkingRoute([from, to], (route) => {
                         let newLeg =
-                            this._createWalkingLeg(from, to, from.place.name,
-                                                   toName, route);
+                            GraphHopperTransit.createWalkingLeg(from, to,
+                                                                from.place.name,
+                                                                toName, route);
                         itinerary.legs[index] = newLeg;
                         this._recalculateItineraryRecursive(itinerary, index + 1,
                                                             callback);
@@ -951,16 +835,19 @@ var OpenTripPlanner = class OpenTripPlanner {
                      * by GH in case the OTP starting point as far enough from
                      * the original starting point
                      */
-                    let to = this._createQueryPointForCoord(leg.fromCoordinate);
+                    let to = GraphHopperTransit.createQueryPointForCoord(leg.fromCoordinate);
                     let fromLoc = from.place.location;
                     let toLoc = to.place.location;
                     let distance = fromLoc.get_distance_from(toLoc) * 1000;
 
                     if (distance >= MIN_WALK_ROUTING_DISTANCE) {
-                        this._fetchWalkingRoute([from, to], (route) => {
+                        GraphHopperTransit.fetchWalkingRoute([from, to],
+                                                             (route) => {
                             let newLeg =
-                                this._createWalkingLeg(from, to, from.place.name,
-                                                       leg.from, route);
+                                GraphHopperTransit.createWalkingLeg(from, to,
+                                                                    from.place.name,
+                                                                    leg.from,
+                                                                    route);
                             itinerary.legs.unshift(newLeg);
                             /* now, next index will be two steps up, since we
                              * inserted a new leg
@@ -977,8 +864,9 @@ var OpenTripPlanner = class OpenTripPlanner {
             } else if (index === itinerary.legs.length - 1) {
                 let to = this._query.filledPoints.last();
                 let startLeg =
-                    this._createQueryPointForCoord(leg.fromCoordinate);
-                let endLeg = this._createQueryPointForCoord(leg.toCoordinate);
+                    GraphHopperTransit.createQueryPointForCoord(leg.fromCoordinate);
+                let endLeg =
+                    GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
                 let toLoc = to.place.location;
                 let startLegLoc = startLeg.place.location;
                 let endLegLoc = endLeg.place.location;
@@ -1017,10 +905,11 @@ var OpenTripPlanner = class OpenTripPlanner {
                         finalTransitLeg = leg;
                         insertIndex = index;
                     }
-                    let from = this._createQueryPointForCoord(finalTransitLeg.fromCoordinate);
-                    this._fetchWalkingRoute([from, to], (route) => {
+                    let from =
+                        GraphHopperTransit.createQueryPointForCoord(finalTransitLeg.fromCoordinate);
+                    GraphHopperTransit.fetchWalkingRoute([from, to], (route) => {
                         let newLeg =
-                            this._createWalkingLeg(from, to,
+                            GraphHopperTransit.createWalkingLeg(from, to,
                                                    finalTransitLeg.from,
                                                    to.place.name, route);
                         itinerary.legs[insertIndex] = newLeg;
@@ -1033,16 +922,17 @@ var OpenTripPlanner = class OpenTripPlanner {
                      * case the OTP end point as far enough from the original
                      * end point
                      */
-                    let from = this._createQueryPointForCoord(leg.toCoordinate);
+                    let from = GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
                     let fromLoc = from.place.location;
                     let toLoc = to.place.location;
                     let distance = fromLoc.get_distance_from(toLoc) * 1000;
 
                     if (distance >= MIN_WALK_ROUTING_DISTANCE) {
-                        this._fetchWalkingRoute([from, to], (route) => {
+                        GraphHopperTransit.fetchWalkingRoute([from, to],
+                                                             (route) => {
                             let newLeg =
-                                this._createWalkingLeg(from, to, leg.to,
-                                                       to.place.name, route);
+                                GraphHopperTransit.createWalkingLeg(from, to,
+                                                leg.to, to.place.name, route);
                             itinerary.legs.push(newLeg);
                             /* now, next index will be two steps up, since we
                              * inserted a new leg
@@ -1061,8 +951,8 @@ var OpenTripPlanner = class OpenTripPlanner {
                  * above the threashhold distance, calculate an exact route
                  */
                 if (!leg.transit && leg.distance >= MIN_WALK_ROUTING_DISTANCE) {
-                    let from = this._createQueryPointForCoord(leg.fromCoordinate);
-                    let to = this._createQueryPointForCoord(leg.toCoordinate);
+                    let from = GraphHopperTransit.createQueryPointForCoord(leg.fromCoordinate);
+                    let to = GraphHopperTransit.createQueryPointForCoord(leg.toCoordinate);
 
                     /* if the next leg is the final one of the itinerary,
                      * and it's shorter than the "optimize away" distance,
@@ -1075,9 +965,10 @@ var OpenTripPlanner = class OpenTripPlanner {
                         itinerary.legs.splice(index + 1, index + 1);
                     }
 
-                    this._fetchWalkingRoute([from, to], (route) => {
-                        let newLeg = this._createWalkingLeg(from, to, leg.from,
-                                                            leg.to, route);
+                    GraphHopperTransit.fetchWalkingRoute([from, to], (route) => {
+                        let newLeg =
+                            GraphHopperTransit.createWalkingLeg(from, to, leg.from,
+                                                                leg.to, route);
                         itinerary.legs[index] = newLeg;
                         this._recalculateItineraryRecursive(itinerary,
                                                             index + 1,
@@ -1093,17 +984,6 @@ var OpenTripPlanner = class OpenTripPlanner {
         }
     }
 
-    _getRoutersForPoints(points) {
-        let startRouters = this._getRoutersForPlace(points[0].place);
-        let endRouters =
-            this._getRoutersForPlace(points.last().place);
-
-        let intersectingRouters =
-            this._routerIntersection(startRouters, endRouters);
-
-        return intersectingRouters;
-    }
-
     _createItineraries(itineraries) {
         return itineraries.map((itinerary) => this._createItinerary(itinerary));
     }
@@ -1118,7 +998,7 @@ var OpenTripPlanner = class OpenTripPlanner {
     }
 
     _createLegs(legs) {
-        return legs.map((leg) => this._createLeg(leg));
+        return legs.map((leg, index, legs) => this._createLeg(leg, index, legs));
     }
 
     /* check if a string is a valid hex RGB string */
@@ -1132,14 +1012,60 @@ var OpenTripPlanner = class OpenTripPlanner {
         return false;
     }
 
-    _createLeg(leg) {
+    _createLeg(leg, index, legs) {
         let polyline = EPAF.decode(leg.legGeometry.points);
-        let intermediateStops =
-            this._createIntermediateStops(leg);
         let color = leg.routeColor && this._isValidHexColor(leg.routeColor) ?
                     leg.routeColor : null;
         let textColor = leg.routeTextColor && this._isValidHexColor(leg.routeTextColor) ?
                         leg.routeTextColor : null;
+        let first = index === 0;
+        let last = index === legs.length - 1;
+        /* for walking legs in the beginning or end, use the name from the
+         * query, so we get the names of the place the user searched for in
+         * the results, when starting/ending at a transitstop, use the stop
+         * name
+         */
+        let from =
+            first && !leg.transitLeg ? this._query.filledPoints[0].place.name :
+                                       leg.from.name;
+        let to =
+            last && !leg.transitLeg ? this._query.filledPoints.last().place.name :
+                                      leg.to.name;
+
+        let result = new TransitPlan.Leg({ departure:            leg.from.departure,
+                                           arrival:              leg.to.arrival,
+                                           from:                 from,
+                                           to:                   to,
+                                           headsign:             leg.headsign,
+                                           fromCoordinate:       [leg.from.lat,
+                                                                  leg.from.lon],
+                                           toCoordinate:         [leg.to.lat,
+                                                                  leg.to.lon],
+                                           route:                leg.route,
+                                           routeType:            leg.routeType,
+                                           polyline:             polyline,
+                                           isTransit:            leg.transitLeg,
+                                           distance:             leg.distance,
+                                           duration:             leg.duration,
+                                           agencyName:           leg.agencyName,
+                                           agencyUrl:            leg.agencyUrl,
+                                           agencyTimezoneOffset: leg.agencyTimeZoneOffset,
+                                           color:                color,
+                                           textColor:            textColor,
+                                           tripShortName:        leg.tripShortName });
+
+        if (leg.transitLeg)
+            result.intermediateStops = this._createIntermediateStops(leg);
+        else if (!this._onlyTransitData)
+            result.walkingInstructions = this._createTurnpoints(leg, polyline);
+
+        return result;
+    }
+
+    _createIntermediateStops(leg) {
+        let stops = leg.intermediateStops;
+        let intermediateStops =
+            stops.map((stop) => this._createIntermediateStop(stop, leg));
 
         /* instroduce an extra stop at the end (in additional to the
          * intermediate stops we get from OTP
@@ -1149,34 +1075,7 @@ var OpenTripPlanner = class OpenTripPlanner {
                                                       agencyTimezoneOffset: leg.agencyTimeZoneOffset,
                                                       coordinate: [leg.to.lat,
                                                                    leg.to.lon] }));
-
-        return new TransitPlan.Leg({ departure:            leg.from.departure,
-                                     arrival:              leg.to.arrival,
-                                     from:                 leg.from.name,
-                                     to:                   leg.to.name,
-                                     headsign:             leg.headsign,
-                                     intermediateStops:    intermediateStops,
-                                     fromCoordinate:       [leg.from.lat,
-                                                            leg.from.lon],
-                                     toCoordinate:         [leg.to.lat,
-                                                            leg.to.lon],
-                                     route:                leg.route,
-                                     routeType:            leg.routeType,
-                                     polyline:             polyline,
-                                     isTransit:            leg.transitLeg,
-                                     distance:             leg.distance,
-                                     duration:             leg.duration,
-                                     agencyName:           leg.agencyName,
-                                     agencyUrl:            leg.agencyUrl,
-                                     agencyTimezoneOffset: leg.agencyTimeZoneOffset,
-                                     color:                color,
-                                     textColor:            textColor,
-                                     tripShortName:        leg.tripShortName });
-    }
-
-    _createIntermediateStops(leg) {
-        let stops = leg.intermediateStops;
-        return stops.map((stop) => this._createIntermediateStop(stop, leg));
+        return intermediateStops;
     }
 
     _createIntermediateStop(stop, leg) {
@@ -1186,4 +1085,154 @@ var OpenTripPlanner = class OpenTripPlanner {
                                       agencyTimezoneOffset: leg.agencyTimeZoneOffset,
                                       coordinate: [stop.lat, stop.lon] });
     }
+
+    /**
+     * Create a turnpoints list on the same format we use with GraphHopper
+     * from OpenTripPlanner walking steps
+     */
+    _createTurnpoints(leg, polyline) {
+        if (leg.steps) {
+            let steps = leg.steps;
+            let startPoint = new Route.TurnPoint({
+                coordinate:  polyline[0],
+                type:        Route.TurnPointType.START,
+                distance:    0,
+                instruction: _("Start!"),
+                time:        0,
+                turnAngle:   0
+            });
+            let turnpoints = [startPoint];
+            steps.forEach((step) => {
+                turnpoints.push(this._createTurnpoint(step));
+            });
+
+            let endPoint = new Route.TurnPoint({
+                coordinate: polyline.last(),
+                type:       Route.TurnPoint.END,
+                distance:   0,
+                instruction:_("Arrive")
+            });
+
+            turnpoints.push(endPoint);
+
+            return turnpoints;
+        } else {
+            return null;
+        }
+    }
+
+    _createTurnpoint(step) {
+        let coordinate = new Champlain.Coordinate({ latitude: step.lat,
+                                                    longitude: step.lon });
+        let turnpoint = new Route.TurnPoint({
+            coordinate: coordinate,
+            type: this._getTurnpointType(step),
+            distance: step.distance,
+            instruction: this._getTurnpointInstruction(step)
+        });
+
+        return turnpoint;
+    }
+
+    _getTurnpointType(step) {
+        switch (step.relativeDirection) {
+            case 'DEPART':
+            case 'CONTINUE':
+                return Route.TurnPointType.CONTINUE;
+            case 'LEFT':
+                return Route.TurnPointType.LEFT;
+            case 'SLIGHTLY_LEFT':
+                return Route.TurnPointType.SLIGHT_LEFT;
+            case 'HARD_LEFT':
+                return Route.TurnPointType.SHARP_LEFT;
+            case 'RIGHT':
+                return Route.TurnPointType.RIGHT;
+            case 'SLIGHTLY_RIGHT':
+                return Route.TurnPointType.SLIGHT_RIGHT;
+            case 'HARD_RIGHT':
+                return Route.TurnPointType.SHARP_RIGHT;
+            case 'CIRCLE_CLOCKWISE':
+            case 'CIRCLE_COUNTERCLOCKWISE':
+                return Route.TurnPointType.ROUNDABOUT;
+            case 'ELEVATOR':
+                return Route.TurnPointType.ELEVATOR;
+            case 'UTURN_LEFT':
+                return Route.TurnPointType.UTURN_LEFT;
+            case 'UTURN_RIGHT':
+                return Route.TurnPointType.UTURN_RIGHT;
+            default:
+                return null;
+        }
+    }
+
+    _getTurnpointInstruction(step) {
+        let street = !step.bogusName ? step.streetName : null;
+        switch (step.relativeDirection) {
+            case 'DEPART':
+            case 'CONTINUE':
+                if (street)
+                    return _("Continue on %s").format(street);
+                else
+                    return _("Continue");
+            case 'LEFT':
+                if (street)
+                    return _("Turn left on %s").format(street);
+                else
+                    return _("Turn left");
+            case 'SLIGHTLY_LEFT':
+                if (street)
+                    return _("Turn slightly left on %s").format(street);
+                else
+                    return _("Turn slightly left");
+            case 'HARD_LEFT':
+                if (street)
+                    return _("Turn sharp left on %s").format(street);
+                else
+                    return _("Turn sharp left");
+            case 'RIGHT':
+                if (street)
+                    return _("Turn right on %s").format(street);
+                else
+                    return _("Turn right");
+            case 'SLIGHTLY_RIGHT':
+                if (street)
+                    return _("Turn slightly right on %s").format(street);
+                else
+                    return _("Turn slightly right");
+            case 'HARD_RIGHT':
+                if (street)
+                    return _("Turn sharp right on %s").format(street);
+                else
+                    return _("Turn sharp right");
+            case 'CIRCLE_CLOCKWISE':
+            case 'CIRCLE_COUNTERCLOCKWISE': {
+                let exit = step.exit;
+
+                if (exit)
+                    return _("In the roundaboat, take exit %s").format(exit);
+                else if (street)
+                    return _("In the roundabout, take exit to %s").format(street);
+                else
+                    return _("Take the roundabout");
+            }
+            case 'ELEVATOR': {
+                if (street)
+                    return _("Take the elevator and get off at %s").format(street);
+                else
+                    return _("Take the elevator");
+            }
+            case 'UTURN_LEFT':
+                if (street)
+                    return _("Make a left u-turn onto %s").format(street);
+                else
+                    return _("Make a left u-turn");
+            case 'UTURN_RIGHT':
+                if (street)
+                    return _("Make a right u-turn onto %s").format(street);
+                else
+                    return _("Make a rigth u-turn");
+            default:
+                return '';
+        }
+    }
 };
diff --git a/src/transitplugins/resrobot.js b/src/transitplugins/resrobot.js
new file mode 100644
index 0000000..20493d4
--- /dev/null
+++ b/src/transitplugins/resrobot.js
@@ -0,0 +1,639 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2019 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+/**
+ * This module implements a transit routing plugin for the Swedish national
+ * Resrobot transit journey planning API.
+ *
+ * API docs for Resrobot can be found at:
+ * https://www.trafiklab.se/api/resrobot-reseplanerare/dokumentation/sokresa
+ */
+
+const Champlain = imports.gi.Champlain;
+const GLib = imports.gi.GLib;
+const Soup = imports.gi.Soup;
+
+const Application = imports.application;
+const GraphHopperTransit = imports.graphHopperTransit;
+const HTTP = imports.http;
+const HVT = imports.hvt;
+const TransitPlan = imports.transitPlan;
+const TransitTweaks = imports.transitTweaks;
+const Utils = imports.utils;
+
+const BASE_URL = 'https://api.resrobot.se';
+const API_VERSION = 'v2';
+
+// Timezone for timestamps returned by this provider
+const NATIVE_TIMEZONE = 'Europe/Stockholm';
+
+const ISO_8601_DURATION_REGEXP = new RegExp(/P((\d+)D)?T((\d+)H)?((\d+)M)?/);
+
+const Products = {
+    EXPRESS_TRAIN:  2,
+    REGIONAL_TRAIN: 4,
+    EXPRESS_BUS:    8,
+    LOCAL_TRAIN:    16,
+    SUBWAY:         32,
+    TRAM:           64,
+    BUS:            128,
+    FERRY:          256,
+    TAXI:           512
+};
+
+const LegType = {
+    WALK:     'WALK',
+    TRANSIT:  'JNY',
+    TRANSFER: 'TRSF'
+};
+
+const CatCode = {
+    EXPRESS_TRAIN:  1,
+    REGIONAL_TRAIN: 2,
+    EXPRESS_BUS:    3,
+    LOCAL_TRAIN:    4,
+    SUBWAY:         5,
+    TRAM:           6,
+    BUS:            7,
+    FERRY:          8,
+    TAXI:           9
+};
+
+const MAX_NUM_NEARBY_STOPS = 5;
+const NEARBY_STOPS_SEARCH_RADIUS = 500;
+
+// ignore walking legs at the beginning/end when below this distance
+const DISTANCE_THREASHOLD_TO_IGNORE = 50;
+
+// search radius to search for walk-only journeys
+const WALK_SEARCH_RADIUS = 2000;
+
+// maximum distance for walk-only journey
+const MAX_WALK_ONLY_DISTANCE = 2500;
+
+var Resrobot = class Resrobot {
+    constructor(params) {
+        this._session = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version });
+        this._plan = Application.routingDelegator.transitRouter.plan;
+        this._query = Application.routeQuery;
+        this._key = params.key;
+        this._tz = GLib.TimeZone.new(NATIVE_TIMEZONE);
+        this._tweaks = new TransitTweaks.TransitTweaks({ name: 'resrobot' });
+
+        if (!this._key)
+            throw new Error('missing key');
+    }
+
+    fetchFirstResults() {
+        let filledPoints = this._query.filledPoints;
+
+        this._extendPrevious = false;
+        this._viaId = null;
+
+        if (filledPoints.length > 3) {
+            Utils.debug('This plugin supports at most one via location');
+            this._plan.reset();
+            this._plan.requestFailed();
+            this._query.reset();
+        } else if (filledPoints.length === 2) {
+            this._fetchResults();
+        } else {
+            let lat = filledPoints[1].place.location.latitude;
+            let lon = filledPoints[1].place.location.longitude;
+
+            this._fetchNearbyStops(lat, lon, MAX_NUM_NEARBY_STOPS,
+                                   NEARBY_STOPS_SEARCH_RADIUS,
+                                   () => this._fetchResults());
+        }
+    }
+
+    fetchMoreResults() {
+        this._extendPrevious = true;
+
+        if ((!this._scrF && !this._query.arriveBy) ||
+            (!this._scrB && this._query.arriveBy))
+            this._noRouteFound();
+        else
+            this._fetchResults();
+    }
+
+    _fetchNearbyStops(lat, lon, num, radius, callback) {
+        let query = new HTTP.Query(this._getNearbyStopsQueryParams(lat, lon,
+                                                                   num, radius));
+        let uri = new Soup.URI(BASE_URL + '/' + API_VERSION +
+                               '/location.nearbystops?' + query.toString());
+        let request = new Soup.Message({ method: 'GET', uri: uri });
+
+        this._session.queue_message(request, (obj, message) => {
+            if (message.status_code !== Soup.Status.OK) {
+                Utils.debug('Failed to get nearby stops: ' + message.status_code);
+                this._noRouteFound();
+            } else {
+                try {
+                    let result = JSON.parse(message.response_body.data);
+                    let stopLocations = result.StopLocation;
+
+                    Utils.debug('nearby stops: ' + JSON.stringify(result, null, 2));
+
+                    if (stopLocations && stopLocations.length > 0) {
+                        let stopLocation = stopLocations[0];
+
+                        this._viaId = stopLocation.id;
+                        callback();
+                    } else {
+                        Utils.debug('No nearby stops found');
+                        this._noRouteFound();
+                    }
+                } catch (e) {
+                    Utils.debug('Error parsing result: ' + e);
+                    this._plan.reset();
+                    this._plan.requestFailed();
+                }
+            }
+        });
+    }
+
+    _fetchResults() {
+        let query = new HTTP.Query(this._getQueryParams());
+        let uri = new Soup.URI(BASE_URL + '/' + API_VERSION + '/trip?' +
+                               query.toString());
+        let request = new Soup.Message({ method: 'GET', uri: uri });
+
+        this._session.queue_message(request, (obj, message) => {
+            if (message.status_code !== Soup.Status.OK) {
+                Utils.debug('Failed to get trip: ' + message.status_code);
+                /* No routes found. If this is the first search
+                 * (not "load more") and the distance is short
+                 * enough, generate a walk-only itinerary
+                 */
+                let [start, end, distance] =
+                    this._getAsTheCrowFliesPointsAndDistanceForQuery();
+
+                if (!this._extendPrevious &&
+                    distance <= WALK_SEARCH_RADIUS) {
+                    GraphHopperTransit.fetchWalkingRoute([start, end], (route) => {
+                        if (route && route.distance <= MAX_WALK_ONLY_DISTANCE) {
+                            let walkingItinerary =
+                                this._createWalkingOnlyItinerary(start,
+                                                                 end,
+                                                                 route);
+                            this._plan.updateWithNewItineraries([walkingItinerary]);
+                        } else {
+                            this._noRouteFound();
+                        }
+                    });
+                } else {
+                    this._noRouteFound();
+                }
+            } else {
+                try {
+                    let result = JSON.parse(message.response_body.data);
+
+                    Utils.debug('result: ' + JSON.stringify(result, null, 2));
+                    if (result.Trip) {
+                        let itineraries = this._createItineraries(result.Trip);
+
+                        // store the back and forward references from the result
+                        this._scrB = result.scrB;
+                        this._scrF = result.scrF;
+                        this._tweaks.applyTweaks(itineraries, () => {
+                            this._processItineraries(itineraries)
+                        });
+                    } else {
+                        this._noRouteFound();
+                    }
+                } catch (e) {
+                    Utils.debug('Error parsing result: ' + e);
+                    this._plan.reset();
+                    this._plan.requestFailed();
+                }
+            }
+        });
+    }
+
+    /* get total "as the crow flies" start, and end points, and distance for
+     * the query
+     */
+    _getAsTheCrowFliesPointsAndDistanceForQuery() {
+        let start = this._query.filledPoints[0];
+        let end = this._query.filledPoints.last();
+        let startLoc = start.place.location;
+        let endLoc = end.place.location;
+
+        return [start, end, endLoc.get_distance_from(startLoc) * 1000];
+    }
+
+    _processItineraries(itineraries) {
+        /* if this is the first request, and the distance is short enough,
+         * add an additional walking-only itinerary at the beginning
+         */
+        let [start, end, distance] =
+            this._getAsTheCrowFliesPointsAndDistanceForQuery();
+
+        if (!this._extendPrevious && distance <= WALK_SEARCH_RADIUS) {
+            GraphHopperTransit.fetchWalkingRoute([start, end], (route) => {
+                if (route && route.distance <= MAX_WALK_ONLY_DISTANCE) {
+                    let walkingItinerary =
+                        this._createWalkingOnlyItinerary(start, end, route);
+
+                    itineraries.unshift(walkingItinerary);
+                }
+                GraphHopperTransit.addWalkingToItineraries(itineraries,
+                    () => this._plan.updateWithNewItineraries(itineraries,
+                                                              this._query.arriveBy,
+                                                              this._extendPrevious));
+            });
+        } else {
+            GraphHopperTransit.addWalkingToItineraries(itineraries,
+                () => this._plan.updateWithNewItineraries(itineraries,
+                                                          this._query.arriveBy,
+                                                          this._extendPrevious));
+        }
+    }
+
+    _createWalkingOnlyItinerary(start, end, route) {
+        let walkingLeg = GraphHopperTransit.createWalkingLeg(start, end,
+                                                             start.place.name,
+                                                             end.place.name,
+                                                             route);
+        let duration = route.duration;
+        /* if the query has no date, just use a fake, since only the time
+         * is relevant for displaying in this case
+         */
+        let date = this._query.date || '2019-01-01';
+        let time = this._query.time + ':00';
+
+        let [timestamp, tzOffset] =
+            this._query.time ? this._parseTime(time, date) :
+                               this._getTimestampAndTzOffsetNow();
+
+        if (this._query.arriveBy) {
+            walkingLeg.arrival = timestamp;
+            walkingLeg.departure = timestamp - route.time;
+        } else {
+            walkingLeg.departure = timestamp;
+            walkingLeg.arrival = timestamp + route.time;
+        }
+
+        walkingLeg.agencyTimezoneOffset = tzOffset;
+
+        let walkingItinerary =
+            new TransitPlan.Itinerary({ legs: [walkingLeg]} );
+
+        walkingItinerary.adjustTimings();
+
+        return walkingItinerary;
+    }
+
+    _reset() {
+        if (this._query.latest)
+            this._query.latest.place = null;
+        else
+            this._plan.reset();
+    }
+
+    /* Indicate that no routes where found, either shows the "No route found"
+     * message, or in case of loading additional (later/earlier) results,
+     * indicate no such where found, so that the sidebar can disable the
+     * "load more" functionallity as appropriate.
+     */
+    _noRouteFound() {
+        if (this._extendPrevious) {
+            this._plan.noMoreResults();
+        } else {
+            this._reset();
+            this._plan.noRouteFound();
+        }
+    }
+
+    _createItineraries(trips) {
+        return trips.map((trip) => this._createItinerary(trip));
+    }
+
+    _createItinerary(trip) {
+        let legs = this._createLegs(trip.LegList.Leg);
+        let duration = this._parseDuration(trip.duration);
+        let origin = trip.LegList.Leg[0].Origin;
+        let destination = trip.LegList.Leg.last().Destination;
+        let [startTime,] = this._parseTime(origin.time, origin.date);
+        let [endTime,] = this._parseTime(destination.time, destination.date);
+
+        return new TransitPlan.Itinerary({ duration:  duration,
+                                           departure: startTime,
+                                           arrival:   endTime,
+                                           legs:      legs,
+                                           duration:  duration });
+    }
+
+    /**
+     * Parse a time and date string into a timestamp into an array with
+     * an absolute timestamp in ms since Unix epoch and a timezone offset
+     * for the provider's native timezone at the given time and date
+     */
+    _parseTime(time, date) {
+        let timeText = '%sT%s'.format(date, time);
+        let dateTime = GLib.DateTime.new_from_iso8601(timeText, this._tz);
+
+        return [dateTime.to_unix() * 1000, dateTime.get_utc_offset() / 1000];
+    }
+
+    /**
+     * Get absolute timestamp for "now" in ms and timezone offset in the
+     * native timezone of the provider's native timezone @ "now"
+     */
+    _getTimestampAndTzOffsetNow() {
+        let dateTime = GLib.DateTime.new_now(this._tz);
+
+        return [dateTime.to_unix() * 1000, dateTime.get_utc_offset() / 1000];
+    }
+
+    /**
+     * Parse a subset of ISO 8601 duration expressions.
+     * Handle hour and minute parts
+     */
+    _parseDuration(duration) {
+        let match = duration.match(ISO_8601_DURATION_REGEXP);
+
+        if (match) {
+            let [,,d,,h,,min] = match;
+
+            return (d || 0) * 86400 + (h || 0) * 3600 + (min || 0) * 60;
+        } else {
+            Utils.debug('Unknown duration: ' + duration);
+
+            return -1;
+        }
+    }
+
+    _createLegs(legs) {
+        let result = legs.map((leg, index, legs) => this._createLeg(leg, index, legs));
+
+        if (this._canLegBeIgnored(result[0]))
+            result.shift();
+
+        if (this._canLegBeIgnored(result.last()))
+            result.splice(-1);
+
+        return result;
+    }
+
+    /* determines if a leg can ignored at the start or end, to catch the
+     * case when the user probably meant to search for a trip from a transit
+     * stop anyway
+     */
+    _canLegBeIgnored(leg) {
+        if (!leg.isTransit) {
+            /* check that the distance is below the threashold and also that
+             * the duration is below 1 min, since the API in some occasions
+             * apparently gives distance 0, even though a walking leg has
+             * longer duration, and spans a distance in coordinates.
+             */
+            return leg.distance <= DISTANCE_THREASHOLD_TO_IGNORE &&
+                   leg.duration <= 60;
+        } else {
+            return false;
+        }
+    }
+
+    _createLeg(leg, index, legs) {
+        let isTransit;
+
+        if (leg.type === LegType.TRANSIT)
+            isTransit = true;
+        else if (leg.type === LegType.WALK || leg.type === LegType.TRANSFER)
+            isTransit = false;
+        else
+            throw new Error('Unknown leg type: ' + leg.type);
+
+        let origin = leg.Origin;
+        let destination = leg.Destination;
+        let product = leg.Product;
+
+        if (!origin)
+            throw new Error('Missing Origin element');
+        if (!destination)
+            throw new Error('Missing Destination element');
+        if (!product && isTransit)
+            throw new Error('Missing Product element for transit leg');
+
+        let first = index === 0;
+        let last = index === legs.length - 1;
+        /* for walking legs in the beginning or end, use the name from the
+         * query, so we get the names of the place the user searched for in
+         * the results, when starting/ending at a transitstop, use the stop
+         * name
+         */
+        let from =
+            first && !isTransit ? this._query.filledPoints[0].place.name :
+                                  origin.name;
+        let to =
+            last && !isTransit ? this._query.filledPoints.last().place.name :
+                                 destination.name;
+        let [departure, tzOffset] = this._parseTime(origin.time, origin.date);
+        let [arrival,] = this._parseTime(destination.time, destination.date);
+        let route = isTransit ? product.num : null;
+        let routeType =
+            isTransit ? this._getHVTCodeFromCatCode(product.catCode) : null;
+        let agencyName = isTransit ? product.operator : null;
+        let agencyUrl = isTransit ? product.operatorUrl : null;
+        let polyline = this._createPolylineForLeg(leg);
+        let duration = leg.duration ? this._parseDuration(leg.duration) : null;
+
+        let result = new TransitPlan.Leg({ departure:            departure,
+                                           arrival:              arrival,
+                                           from:                 from,
+                                           to:                   to,
+                                           headsign:             leg.direction,
+                                           fromCoordinate:       [origin.lat,
+                                                                  origin.lon],
+                                           toCoordinate:         [destination.lat,
+                                                                  destination.lon],
+                                           route:                route,
+                                           routeType:            routeType,
+                                           polyline:             polyline,
+                                           isTransit:            isTransit,
+                                           distance:             leg.dist,
+                                           duration:             duration,
+                                           agencyName:           agencyName,
+                                           agencyUrl:            agencyUrl,
+                                           agencyTimezoneOffset: tzOffset,
+                                           tripShortName:        route });
+
+        if (isTransit)
+            result.intermediateStops = this._createIntermediateStops(leg);
+
+        return result;
+    }
+
+    _createPolylineForLeg(leg) {
+        let polyline;
+
+        if (leg.Stops && leg.Stops.Stop) {
+            polyline = [];
+
+            leg.Stops.Stop.forEach((stop) => {
+                polyline.push(new Champlain.Coordinate({ latitude:  stop.lat,
+                                                         longitude: stop.lon }));
+            });
+        } else {
+            polyline =
+                [new Champlain.Coordinate({ latitude:  leg.Origin.lat,
+                                            longitude: leg.Origin.lon }),
+                 new Champlain.Coordinate({ latitude:  leg.Destination.lat,
+                                            longitude: leg.Destination.lon })];
+        }
+
+        return polyline;
+    }
+
+    _createIntermediateStops(leg) {
+        let result = [];
+
+        if (!leg.Stops && !leg.Stops.Stop)
+            throw new Error('Missing Stops element');
+
+        leg.Stops.Stop.forEach((stop, index) => {
+            if (index !== 0)
+                result.push(this._createIntermediateStop(stop));
+        });
+
+        return result;
+    }
+
+    _createIntermediateStop(stop) {
+        let [departure, departureTzOffset] = [,];
+        let [arrival, arrivalTzOffset] = [,];
+
+        if (stop.depTime && stop.depDate)
+            [departure, departureTzOffset] = this._parseTime(stop.depTime, stop.depDate);
+        if (stop.arrTime && stop.arrDate)
+            [arrival, arrivalTzOffset] = this._parseTime(stop.arrTime, stop.arrDate);
+
+        if (!arrival)
+            arrival = departure;
+        if (!departure)
+            departure = arrival;
+
+        return new TransitPlan.Stop({ name:                 stop.name,
+                                      arrival:              arrival,
+                                      departure:            departure,
+                                      agencyTimezoneOffset: departureTzOffset || arrivalTzOffset,
+                                      coordinate: [stop.lat, stop.lon] });
+    }
+
+    _getHVTCodeFromCatCode(code) {
+        switch (parseInt(code)) {
+            case CatCode.EXPRESS_TRAIN:
+                return HVT.HIGH_SPEED_RAIL_SERVICE;
+            case CatCode.REGIONAL_TRAIN:
+                return HVT.REGIONAL_RAIL_SERVICE;
+            case CatCode.EXPRESS_BUS:
+                return HVT.EXPRESS_BUS_SERVICE;
+            case CatCode.LOCAL_TRAIN:
+                return HVT.SUBURBAN_RAILWAY_SERVICE;
+            case CatCode.SUBWAY:
+                return HVT.METRO_SERVICE;
+            case CatCode.TRAM:
+                return HVT.TRAM_SERVICE;
+            case CatCode.BUS:
+                return HVT.BUS_SERVICE;
+            case CatCode.FERRY:
+                return HVT.WATER_TRANSPORT_SERVICE;
+            case CatCode.TAXI:
+                return HVT.COMMUNAL_TAXI_SERVICE;
+            default:
+                Utils.debug('Unknown catCode: ' + code);
+                return HVT.MISCELLANEOUS_SERVICE;
+        }
+    }
+
+    _getQueryParams() {
+        let points = this._query.filledPoints;
+        let originLocation = points[0].place.location;
+        let destLocation = points.last().place.location;
+        let transitOptions = this._query.transitOptions;
+        let params = { key:             this._key,
+                       originCoordLat:  originLocation.latitude,
+                       originCoordLong: originLocation.longitude,
+                       destCoordLat:    destLocation.latitude,
+                       destCoordLong:   destLocation.longitude,
+                       format:          'json' };
+
+        if (!transitOptions.showAllTransitTypes)
+            params.products = this._getAllowedProductsForQuery();
+
+        if (this._viaId)
+            params.viaId = this._viaId;
+
+        if (this._extendPrevious) {
+            params.context = this._query.arriveBy ? this._scrB : this._scrF;
+        } else  {
+            if (this._query.arriveBy)
+                params.searchForArrival = 1;
+
+            if (this._query.time)
+                params.time = this._query.time;
+
+            if (this._query.date)
+                params.date = this._query.date;
+        }
+
+        return params;
+    }
+
+    _getNearbyStopsQueryParams(lat, lon, num, radius) {
+        let params = { key:             this._key,
+                       originCoordLat:  lat,
+                       originCoordLong: lon,
+                       maxNo:           num,
+                       r:               radius,
+                       format:          'json' };
+
+        return params;
+    }
+
+    _getAllowedProductsForQuery() {
+        let products = 0;
+
+        this._query.transitOptions.transitTypes.forEach((type) => {
+            products += this._productCodeForTransitType(type);
+        });
+
+        return products;
+    }
+
+    _productCodeForTransitType(type) {
+        switch (type) {
+            case TransitPlan.RouteType.BUS:
+                return Products.BUS + Products.EXPRESS_BUS + Products.TAXI;
+            case TransitPlan.RouteType.TRAM:
+                return Products.TRAM;
+            case TransitPlan.RouteType.TRAIN:
+                return Products.EXPRESS_TRAIN + Products.LOCAL_TRAIN;
+            case TransitPlan.RouteType.SUBWAY:
+                return Products.SUBWAY;
+            case TransitPlan.RouteType.FERRY:
+                return Products.FERRY;
+            default:
+                return 0;
+        }
+    }
+}
diff --git a/src/utils.js b/src/utils.js
index 2407e3c..f781e07 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -213,6 +213,15 @@ function getMeasurementSystem() {
     return measurementSystem;
 }
 
+/**
+ * Get the higest priority bare lange currently in use.
+ */
+function getLanguage() {
+    let locale = GLib.get_language_names()[0];
+    // the last item returned is the "bare" language
+    return GLib.get_locale_variants(locale).slice(-1)[0];
+}
+
 function getAccuracyDescription(accuracy) {
     switch(accuracy) {
     case Geocode.LOCATION_ACCURACY_UNKNOWN:
diff --git a/src/wikipedia.js b/src/wikipedia.js
index 52c567d..48e231c 100644
--- a/src/wikipedia.js
+++ b/src/wikipedia.js
@@ -30,7 +30,7 @@ const Utils = imports.utils;
 let _soupSession = null;
 function _getSoupSession() {
     if (_soupSession === null) {
-        _soupSession = new Soup.Session ();
+        _soupSession = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version });
     }
 
     return _soupSession;
@@ -43,6 +43,11 @@ function getLanguage(wiki) {
 }
 
 function getArticle(wiki) {
+    return Soup.uri_encode(wiki.replace(/ /g, '_').split(':').splice(1).join(':'),
+                           '\'');
+}
+
+function getHtmlEntityEncodedArticle(wiki) {
     return GLib.markup_escape_text(wiki.split(':').splice(1).join(':'), -1);
 }
 
@@ -53,7 +58,7 @@ function getArticle(wiki) {
  */
 function fetchArticleThumbnail(wiki, size, callback) {
     let lang = getLanguage(wiki);
-    let title = getArticle(wiki);
+    let title = getHtmlEntityEncodedArticle(wiki);
     let uri = Format.vprintf('https://%s.wikipedia.org/w/api.php', [ lang ]);
     let msg = Soup.form_request_new_from_hash('GET', uri, { action: 'query',
                                                             titles: title,