Convert `Controller/Map/Route/Planner` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Sat, 2 Jan 2021 12:47:39 +0000 (13:47 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 2 Jan 2021 12:47:39 +0000 (13:47 +0100)
package-lock.json
package.json
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Map/Route/Planner.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.ts [new file with mode: 0644]

index be241647fe225974402be2d4a42963b5b8e94295..f6b9ced5c6b3ee2aae5ae69122a9acf1ff2b5fe7 100644 (file)
       "resolved": "https://registry.npmjs.org/@types/facebook-js-sdk/-/facebook-js-sdk-3.3.1.tgz",
       "integrity": "sha512-jRVPdOu237QxDDoBjc9/xzGsDz75FmdvcwVZdCEg1AjHAQxGmXoHfACUyUVtz7DSWA4E+jgj5MQME4snjGwOng=="
     },
+    "@types/googlemaps": {
+      "version": "3.43.0",
+      "resolved": "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.43.0.tgz",
+      "integrity": "sha512-4k6N1LKTmwHEG6zKNMfINqQS08jd3wYdmlBRfJpcKqUYC9A9uXzuO3irV6muI+PWg1DRAjlBuLfrQTmHNjzlGA==",
+      "dev": true
+    },
     "@types/jquery": {
       "version": "3.5.4",
       "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.4.tgz",
index 24987ad4c3389858cbc835a2213cdb15c251a3f4..0d5efc7c558cf588d9f39354ccf2d4220260a02f 100644 (file)
@@ -2,6 +2,7 @@
   "name": "@woltlab/wcf",
   "version": "5.4.0",
   "devDependencies": {
+    "@types/googlemaps": "^3.43.0",
     "@types/perfect-scrollbar": "^0.6.1",
     "@typescript-eslint/eslint-plugin": "^4.6.0",
     "@typescript-eslint/parser": "^4.6.0",
index 6acb7655d7fd341ba054c245c9533f453bea17e4..f4368facc94f52b947e6c835ef9f98f34d4b11f4 100644 (file)
 /**
  * Map route planner based on Google Maps.
  *
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Controller/Map/Route/Planner
+ * @author  Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Controller/Map/Route/Planner
  */
-define([
-    'Dom/Traverse',
-    'Dom/Util',
-    'Language',
-    'Ui/Dialog',
-    'WoltLabSuite/Core/Ajax/Status'
-], function (DomTraverse, DomUtil, Language, UiDialog, AjaxStatus) {
-    /**
-     * @constructor
-     */
-    function Planner(buttonId, destination) {
-        this._button = elById(buttonId);
-        if (this._button === null) {
-            throw new Error("Unknown button with id '" + buttonId + "'");
+define(["require", "exports", "tslib", "../../../Ajax/Status", "../../../Core", "../../../Dom/Util", "../../../Language", "../../../Ui/Dialog"], function (require, exports, tslib_1, AjaxStatus, Core, Util_1, Language, Dialog_1) {
+    "use strict";
+    AjaxStatus = tslib_1.__importStar(AjaxStatus);
+    Core = tslib_1.__importStar(Core);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    Language = tslib_1.__importStar(Language);
+    Dialog_1 = tslib_1.__importDefault(Dialog_1);
+    class ControllerMapRoutePlanner {
+        constructor(buttonId, destination) {
+            this.didInitDialog = false;
+            this.directionsRenderer = undefined;
+            this.directionsService = undefined;
+            this.googleLink = undefined;
+            this.lastOrigin = undefined;
+            this.map = undefined;
+            this.originInput = undefined;
+            this.travelMode = undefined;
+            const button = document.getElementById(buttonId);
+            if (button === null) {
+                throw new Error(`Unknown button with id '${buttonId}'`);
+            }
+            this.button = button;
+            this.button.addEventListener("click", (ev) => this.openDialog(ev));
+            this.destination = destination;
         }
-        this._button.addEventListener('click', this._openDialog.bind(this));
-        this._destination = destination;
-    }
-    Planner.prototype = {
-        /**
-         * Sets up the route planner dialog.
-         */
-        _dialogSetup: function () {
-            return {
-                id: this._button.id + 'Dialog',
-                options: {
-                    onShow: this._initDialog.bind(this),
-                    title: Language.get('wcf.map.route.planner')
-                },
-                source: '<div class="googleMapsDirectionsContainer" style="display: none;">' +
-                    '<div class="googleMap"></div>' +
-                    '<div class="googleMapsDirections"></div>' +
-                    '</div>' +
-                    '<small class="googleMapsDirectionsGoogleLinkContainer"><a href="' + this._getGoogleMapsLink() + '" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">' + Language.get('wcf.map.route.viewOnGoogleMaps') + '</a></small>' +
-                    '<dl>' +
-                    '<dt>' + Language.get('wcf.map.route.origin') + '</dt>' +
-                    '<dd><input type="text" name="origin" class="long" autofocus /></dd>' +
-                    '</dl>' +
-                    '<dl style="display: none;">' +
-                    '<dt>' + Language.get('wcf.map.route.travelMode') + '</dt>' +
-                    '<dd>' +
-                    '<select name="travelMode">' +
-                    '<option value="driving">' + Language.get('wcf.map.route.travelMode.driving') + '</option>' +
-                    '<option value="walking">' + Language.get('wcf.map.route.travelMode.walking') + '</option>' +
-                    '<option value="bicycling">' + Language.get('wcf.map.route.travelMode.bicycling') + '</option>' +
-                    '<option value="transit">' + Language.get('wcf.map.route.travelMode.transit') + '</option>' +
-                    '</select>' +
-                    '</dd>' +
-                    '</dl>'
-            };
-        },
         /**
          * Calculates the route based on the given result of a location search.
-         *
-         * @param      {object}        data
          */
-        _calculateRoute: function (data) {
-            var dialog = UiDialog.getDialog(this).dialog;
+        _calculateRoute(data) {
+            const dialog = Dialog_1.default.getDialog(this).dialog;
             if (data.label) {
-                this._originInput.value = data.label;
+                this.originInput.value = data.label;
             }
-            if (this._map === undefined) {
-                this._map = new google.maps.Map(elByClass('googleMap', dialog)[0], {
-                    disableDoubleClickZoom: WCF.Location.GoogleMaps.Settings.get('disableDoubleClickZoom'),
-                    draggable: WCF.Location.GoogleMaps.Settings.get('draggable'),
+            if (this.map === undefined) {
+                const mapContainer = dialog.querySelector(".googleMap");
+                this.map = new google.maps.Map(mapContainer, {
+                    disableDoubleClickZoom: window.WCF.Location.GoogleMaps.Settings.get("disableDoubleClickZoom"),
+                    draggable: window.WCF.Location.GoogleMaps.Settings.get("draggable"),
                     mapTypeId: google.maps.MapTypeId.ROADMAP,
-                    scaleControl: WCF.Location.GoogleMaps.Settings.get('scaleControl'),
-                    scrollwheel: WCF.Location.GoogleMaps.Settings.get('scrollwheel')
+                    scaleControl: window.WCF.Location.GoogleMaps.Settings.get("scaleControl"),
+                    scrollwheel: window.WCF.Location.GoogleMaps.Settings.get("scrollwheel"),
                 });
-                this._directionsService = new google.maps.DirectionsService();
-                this._directionsRenderer = new google.maps.DirectionsRenderer();
-                this._directionsRenderer.setMap(this._map);
-                this._directionsRenderer.setPanel(elByClass('googleMapsDirections', dialog)[0]);
-                this._googleLink = elByClass('googleMapsDirectionsGoogleLink', dialog)[0];
+                this.directionsService = new google.maps.DirectionsService();
+                this.directionsRenderer = new google.maps.DirectionsRenderer();
+                this.directionsRenderer.setMap(this.map);
+                const directionsContainer = dialog.querySelector(".googleMapsDirections");
+                this.directionsRenderer.setPanel(directionsContainer);
+                this.googleLink = dialog.querySelector(".googleMapsDirectionsGoogleLink");
             }
-            var request = {
-                destination: this._destination,
+            const request = {
+                destination: this.destination,
                 origin: data.location,
                 provideRouteAlternatives: true,
-                travelMode: google.maps.TravelMode[this._travelMode.value.toUpperCase()]
+                travelMode: google.maps.TravelMode[this.travelMode.value.toUpperCase()],
             };
             AjaxStatus.show();
-            this._directionsService.route(request, this._setRoute.bind(this));
-            elAttr(this._googleLink, 'href', this._getGoogleMapsLink(data.location, this._travelMode.value));
-            this._lastOrigin = data.location;
-        },
+            this.directionsService.route(request, (result, status) => this.setRoute(result, status));
+            this.googleLink.href = this.getGoogleMapsLink(data.location, this.travelMode.value);
+            this.lastOrigin = data.location;
+        }
         /**
          * Returns the Google Maps link based on the given optional directions origin
          * and optional travel mode.
-         *
-         * @param      {google.maps.LatLng}    origin
-         * @param      {string}                travelMode
-         * @return     {string}
          */
-        _getGoogleMapsLink: function (origin, travelMode) {
+        getGoogleMapsLink(origin, travelMode) {
             if (origin) {
-                var link = 'https://www.google.com/maps/dir/?api=1' +
-                    '&origin=' + origin.lat() + ',' + origin.lng() + '' +
-                    '&destination=' + this._destination.lat() + ',' + this._destination.lng();
+                let link = `https://www.google.com/maps/dir/?api=1&origin=${origin.lat()},${origin.lng()}&destination=${this.destination.lat()},${this.destination.lng()}`;
                 if (travelMode) {
-                    link += '&travelmode=' + travelMode;
+                    link += `&travelmode=${travelMode}`;
                 }
                 return link;
             }
-            return 'https://www.google.com/maps/search/?api=1&query=' + this._destination.lat() + ',' + this._destination.lng();
-        },
+            return `https://www.google.com/maps/search/?api=1&query=${this.destination.lat()},${this.destination.lng()}`;
+        }
         /**
          * Initializes the route planning dialog.
          */
-        _initDialog: function () {
-            if (!this._didInitDialog) {
-                var dialog = UiDialog.getDialog(this).dialog;
+        initDialog() {
+            if (!this.didInitDialog) {
+                const dialog = Dialog_1.default.getDialog(this).dialog;
                 // make input element a location search
-                this._originInput = elBySel('input[name="origin"]', dialog);
-                new WCF.Location.GoogleMaps.LocationSearch(this._originInput, this._calculateRoute.bind(this));
-                this._travelMode = elBySel('select[name="travelMode"]', dialog);
-                this._travelMode.addEventListener('change', this._updateRoute.bind(this));
-                this._didInitDialog = true;
+                this.originInput = dialog.querySelector('input[name="origin"]');
+                new window.WCF.Location.GoogleMaps.LocationSearch(this.originInput, (data) => this._calculateRoute(data));
+                this.travelMode = dialog.querySelector('select[name="travelMode"]');
+                this.travelMode.addEventListener("change", this.updateRoute.bind(this));
+                this.didInitDialog = true;
             }
-        },
+        }
         /**
          * Opens the route planning dialog.
          */
-        _openDialog: function () {
-            UiDialog.open(this);
-        },
+        openDialog(event) {
+            event.preventDefault();
+            Dialog_1.default.open(this);
+        }
         /**
          * Handles the response of the direction service.
-         *
-         * @param      {object}        result
-         * @param      {string}        status
          */
-        _setRoute: function (result, status) {
+        setRoute(result, status) {
             AjaxStatus.hide();
-            if (status === 'OK') {
-                elShow(this._map.getDiv().parentNode);
-                google.maps.event.trigger(this._map, 'resize');
-                this._directionsRenderer.setDirections(result);
-                elShow(DomTraverse.parentByTag(this._travelMode, 'DL'));
-                elShow(this._googleLink);
-                elInnerError(this._originInput, false);
+            if (status === "OK") {
+                Util_1.default.show(this.map.getDiv().parentElement);
+                google.maps.event.trigger(this.map, "resize");
+                this.directionsRenderer.setDirections(result);
+                Util_1.default.show(this.travelMode.closest("dl"));
+                Util_1.default.show(this.googleLink);
+                Util_1.default.innerError(this.originInput, false);
             }
             else {
                 // map irrelevant errors to not found error
-                if (status !== 'OVER_QUERY_LIMIT' && status !== 'REQUEST_DENIED') {
-                    status = 'NOT_FOUND';
+                if (status !== "OVER_QUERY_LIMIT" && status !== "REQUEST_DENIED") {
+                    status = google.maps.DirectionsStatus.NOT_FOUND;
                 }
-                elInnerError(this._originInput, Language.get('wcf.map.route.error.' + status.toLowerCase()));
+                Util_1.default.innerError(this.originInput, Language.get(`wcf.map.route.error.${status.toLowerCase()}`));
             }
-        },
+        }
         /**
          * Updates the route after the travel mode has been changed.
          */
-        _updateRoute: function () {
+        updateRoute() {
             this._calculateRoute({
-                location: this._lastOrigin
+                location: this.lastOrigin,
             });
         }
-    };
-    return Planner;
+        /**
+         * Sets up the route planner dialog.
+         */
+        _dialogSetup() {
+            return {
+                id: this.button.id + "Dialog",
+                options: {
+                    onShow: this.initDialog.bind(this),
+                    title: Language.get("wcf.map.route.planner"),
+                },
+                source: `
+<div class="googleMapsDirectionsContainer" style="display: none;">
+  <div class="googleMap"></div>
+  <div class="googleMapsDirections"></div>
+</div>
+<small class="googleMapsDirectionsGoogleLinkContainer">
+  <a href="${this.getGoogleMapsLink()}" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">${Language.get("wcf.map.route.viewOnGoogleMaps")}</a>
+</small>
+<dl>
+  <dt>${Language.get("wcf.map.route.origin")}</dt>
+  <dd>
+    <input type="text" name="origin" class="long" autofocus>
+  </dd>
+</dl>
+<dl style="display: none;">
+  <dt>${Language.get("wcf.map.route.travelMode")}</dt>
+  <dd>
+    <select name="travelMode">
+      <option value="driving">${Language.get("wcf.map.route.travelMode.driving")}</option>
+      <option value="walking">${Language.get("wcf.map.route.travelMode.walking")}</option>
+      <option value="bicycling">${Language.get("wcf.map.route.travelMode.bicycling")}</option>
+      <option value="transit">${Language.get("wcf.map.route.travelMode.transit")}</option>
+    </select>
+  </dd>
+</dl>`,
+            };
+        }
+    }
+    Core.enableLegacyInheritance(ControllerMapRoutePlanner);
+    return ControllerMapRoutePlanner;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.js
deleted file mode 100644 (file)
index 730fbe1..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-/**
- * Map route planner based on Google Maps.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Controller/Map/Route/Planner
- */
-define([
-       'Dom/Traverse',
-       'Dom/Util',
-       'Language',
-       'Ui/Dialog',
-       'WoltLabSuite/Core/Ajax/Status'
-], function(
-       DomTraverse,
-       DomUtil,
-       Language,
-       UiDialog,
-       AjaxStatus
-) {
-       /**
-        * @constructor
-        */
-       function Planner(buttonId, destination) {
-               this._button = elById(buttonId);
-               if (this._button === null) {
-                       throw new Error("Unknown button with id '" + buttonId + "'");
-               }
-               
-               this._button.addEventListener('click', this._openDialog.bind(this));
-               
-               this._destination = destination;
-       }
-       Planner.prototype = {
-               /**
-                * Sets up the route planner dialog.
-                */
-               _dialogSetup: function() {
-                       return {
-                               id: this._button.id + 'Dialog',
-                               options: {
-                                       onShow: this._initDialog.bind(this),
-                                       title: Language.get('wcf.map.route.planner')
-                               },
-                               source: '<div class="googleMapsDirectionsContainer" style="display: none;">' +
-                                               '<div class="googleMap"></div>' +
-                                               '<div class="googleMapsDirections"></div>' +
-                                       '</div>' +
-                                       '<small class="googleMapsDirectionsGoogleLinkContainer"><a href="' + this._getGoogleMapsLink() + '" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">' + Language.get('wcf.map.route.viewOnGoogleMaps') + '</a></small>' +
-                                       '<dl>' +
-                                               '<dt>' + Language.get('wcf.map.route.origin') + '</dt>' +
-                                               '<dd><input type="text" name="origin" class="long" autofocus /></dd>' +
-                                       '</dl>' +
-                                       '<dl style="display: none;">' +
-                                               '<dt>' + Language.get('wcf.map.route.travelMode') + '</dt>' +
-                                               '<dd>' +
-                                                       '<select name="travelMode">' +
-                                                               '<option value="driving">' + Language.get('wcf.map.route.travelMode.driving') + '</option>' + 
-                                                               '<option value="walking">' + Language.get('wcf.map.route.travelMode.walking') + '</option>' + 
-                                                               '<option value="bicycling">' + Language.get('wcf.map.route.travelMode.bicycling') + '</option>' +
-                                                               '<option value="transit">' + Language.get('wcf.map.route.travelMode.transit') + '</option>' +
-                                                       '</select>' +
-                                               '</dd>' +
-                                       '</dl>'
-                       }
-               },
-               
-               /**
-                * Calculates the route based on the given result of a location search.
-                * 
-                * @param       {object}        data
-                */
-               _calculateRoute: function(data) {
-                       var dialog = UiDialog.getDialog(this).dialog;
-                       
-                       if (data.label) {
-                               this._originInput.value = data.label;
-                       }
-                       
-                       if (this._map === undefined) {
-                               this._map = new google.maps.Map(elByClass('googleMap', dialog)[0], {
-                                       disableDoubleClickZoom: WCF.Location.GoogleMaps.Settings.get('disableDoubleClickZoom'),
-                                       draggable: WCF.Location.GoogleMaps.Settings.get('draggable'),
-                                       mapTypeId: google.maps.MapTypeId.ROADMAP,
-                                       scaleControl: WCF.Location.GoogleMaps.Settings.get('scaleControl'),
-                                       scrollwheel: WCF.Location.GoogleMaps.Settings.get('scrollwheel')
-                               });
-                               
-                               this._directionsService = new google.maps.DirectionsService();
-                               this._directionsRenderer = new google.maps.DirectionsRenderer();
-                               
-                               this._directionsRenderer.setMap(this._map);
-                               this._directionsRenderer.setPanel(elByClass('googleMapsDirections', dialog)[0]);
-                               
-                               this._googleLink = elByClass('googleMapsDirectionsGoogleLink', dialog)[0];
-                       }
-                       
-                       var request = {
-                               destination: this._destination,
-                               origin: data.location,
-                               provideRouteAlternatives: true,
-                               travelMode: google.maps.TravelMode[this._travelMode.value.toUpperCase()]
-                       };
-                       
-                       AjaxStatus.show();
-                       this._directionsService.route(request, this._setRoute.bind(this));
-                       
-                       elAttr(this._googleLink, 'href', this._getGoogleMapsLink(data.location, this._travelMode.value));
-                       
-                       this._lastOrigin = data.location;
-               },
-               
-               /**
-                * Returns the Google Maps link based on the given optional directions origin
-                * and optional travel mode.
-                * 
-                * @param       {google.maps.LatLng}    origin
-                * @param       {string}                travelMode
-                * @return      {string}
-                */
-               _getGoogleMapsLink: function(origin, travelMode) {
-                       if (origin) {
-                               var link = 'https://www.google.com/maps/dir/?api=1' +
-                                               '&origin=' + origin.lat() + ',' + origin.lng() + '' +
-                                               '&destination=' + this._destination.lat() + ',' + this._destination.lng();
-                               
-                               if (travelMode) {
-                                       link += '&travelmode=' + travelMode;
-                               }
-                               
-                               return link;
-                       }
-                       
-                       return 'https://www.google.com/maps/search/?api=1&query=' + this._destination.lat() + ',' + this._destination.lng();
-               },
-               
-               /**
-                * Initializes the route planning dialog.
-                */
-               _initDialog: function() {
-                       if (!this._didInitDialog) {
-                               var dialog = UiDialog.getDialog(this).dialog;
-                               
-                               // make input element a location search
-                               this._originInput = elBySel('input[name="origin"]', dialog);
-                               new WCF.Location.GoogleMaps.LocationSearch(this._originInput, this._calculateRoute.bind(this));
-                               
-                               this._travelMode = elBySel('select[name="travelMode"]', dialog);
-                               this._travelMode.addEventListener('change', this._updateRoute.bind(this));
-                               
-                               this._didInitDialog = true;
-                       }
-               },
-               
-               /**
-                * Opens the route planning dialog.
-                */
-               _openDialog: function() {
-                       UiDialog.open(this);
-               },
-               
-               /**
-                * Handles the response of the direction service.
-                * 
-                * @param       {object}        result
-                * @param       {string}        status
-                */
-               _setRoute: function(result, status) {
-                       AjaxStatus.hide();
-                       
-                       if (status === 'OK') {
-                               elShow(this._map.getDiv().parentNode);
-                               
-                               google.maps.event.trigger(this._map, 'resize');
-                               
-                               this._directionsRenderer.setDirections(result);
-                               
-                               elShow(DomTraverse.parentByTag(this._travelMode, 'DL'));
-                               elShow(this._googleLink);
-                               
-                               elInnerError(this._originInput, false);
-                       }
-                       else {
-                               // map irrelevant errors to not found error
-                               if (status !== 'OVER_QUERY_LIMIT' && status !== 'REQUEST_DENIED') {
-                                       status = 'NOT_FOUND';
-                               }
-                               
-                               elInnerError(this._originInput, Language.get('wcf.map.route.error.' + status.toLowerCase()));
-                       }
-               },
-               
-               /**
-                * Updates the route after the travel mode has been changed.
-                */
-               _updateRoute: function() {
-                       this._calculateRoute({
-                               location: this._lastOrigin
-                       });
-               }
-       };
-       
-       return Planner;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Map/Route/Planner.ts
new file mode 100644 (file)
index 0000000..6630979
--- /dev/null
@@ -0,0 +1,215 @@
+/**
+ * Map route planner based on Google Maps.
+ *
+ * @author  Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Controller/Map/Route/Planner
+ */
+
+import * as AjaxStatus from "../../../Ajax/Status";
+import * as Core from "../../../Core";
+import DomUtil from "../../../Dom/Util";
+import * as Language from "../../../Language";
+import UiDialog from "../../../Ui/Dialog";
+import { DialogCallbackObject, DialogCallbackSetup } from "../../../Ui/Dialog/Data";
+
+interface LocationData {
+  label?: string;
+  location: google.maps.LatLng;
+}
+
+class ControllerMapRoutePlanner implements DialogCallbackObject {
+  private readonly button: HTMLElement;
+  private readonly destination: google.maps.LatLng;
+  private didInitDialog = false;
+  private directionsRenderer?: google.maps.DirectionsRenderer = undefined;
+  private directionsService?: google.maps.DirectionsService = undefined;
+  private googleLink?: HTMLAnchorElement = undefined;
+  private lastOrigin?: google.maps.LatLng = undefined;
+  private map?: google.maps.Map = undefined;
+  private originInput?: HTMLInputElement = undefined;
+  private travelMode?: HTMLSelectElement = undefined;
+
+  constructor(buttonId: string, destination: google.maps.LatLng) {
+    const button = document.getElementById(buttonId);
+    if (button === null) {
+      throw new Error(`Unknown button with id '${buttonId}'`);
+    }
+    this.button = button;
+
+    this.button.addEventListener("click", (ev) => this.openDialog(ev));
+
+    this.destination = destination;
+  }
+
+  /**
+   * Calculates the route based on the given result of a location search.
+   */
+  _calculateRoute(data: LocationData): void {
+    const dialog = UiDialog.getDialog(this)!.dialog;
+
+    if (data.label) {
+      this.originInput!.value = data.label;
+    }
+
+    if (this.map === undefined) {
+      const mapContainer = dialog.querySelector(".googleMap") as HTMLElement;
+      this.map = new google.maps.Map(mapContainer, {
+        disableDoubleClickZoom: window.WCF.Location.GoogleMaps.Settings.get("disableDoubleClickZoom"),
+        draggable: window.WCF.Location.GoogleMaps.Settings.get("draggable"),
+        mapTypeId: google.maps.MapTypeId.ROADMAP,
+        scaleControl: window.WCF.Location.GoogleMaps.Settings.get("scaleControl"),
+        scrollwheel: window.WCF.Location.GoogleMaps.Settings.get("scrollwheel"),
+      });
+
+      this.directionsService = new google.maps.DirectionsService();
+      this.directionsRenderer = new google.maps.DirectionsRenderer();
+
+      this.directionsRenderer.setMap(this.map);
+      const directionsContainer = dialog.querySelector(".googleMapsDirections") as HTMLElement;
+      this.directionsRenderer.setPanel(directionsContainer);
+
+      this.googleLink = dialog.querySelector(".googleMapsDirectionsGoogleLink") as HTMLAnchorElement;
+    }
+
+    const request = {
+      destination: this.destination,
+      origin: data.location,
+      provideRouteAlternatives: true,
+      travelMode: google.maps.TravelMode[this.travelMode!.value.toUpperCase()],
+    };
+
+    AjaxStatus.show();
+    this.directionsService!.route(request, (result, status) => this.setRoute(result, status));
+
+    this.googleLink!.href = this.getGoogleMapsLink(data.location, this.travelMode!.value);
+
+    this.lastOrigin = data.location;
+  }
+
+  /**
+   * Returns the Google Maps link based on the given optional directions origin
+   * and optional travel mode.
+   */
+  private getGoogleMapsLink(origin?: google.maps.LatLng, travelMode?: string): string {
+    if (origin) {
+      let link = `https://www.google.com/maps/dir/?api=1&origin=${origin.lat()},${origin.lng()}&destination=${this.destination.lat()},${this.destination.lng()}`;
+
+      if (travelMode) {
+        link += `&travelmode=${travelMode}`;
+      }
+
+      return link;
+    }
+
+    return `https://www.google.com/maps/search/?api=1&query=${this.destination.lat()},${this.destination.lng()}`;
+  }
+
+  /**
+   * Initializes the route planning dialog.
+   */
+  private initDialog(): void {
+    if (!this.didInitDialog) {
+      const dialog = UiDialog.getDialog(this)!.dialog;
+
+      // make input element a location search
+      this.originInput = dialog.querySelector('input[name="origin"]') as HTMLInputElement;
+      new window.WCF.Location.GoogleMaps.LocationSearch(this.originInput, (data) => this._calculateRoute(data));
+
+      this.travelMode = dialog.querySelector('select[name="travelMode"]') as HTMLSelectElement;
+      this.travelMode.addEventListener("change", this.updateRoute.bind(this));
+
+      this.didInitDialog = true;
+    }
+  }
+
+  /**
+   * Opens the route planning dialog.
+   */
+  private openDialog(event: Event): void {
+    event.preventDefault();
+
+    UiDialog.open(this);
+  }
+
+  /**
+   * Handles the response of the direction service.
+   */
+  private setRoute(result: google.maps.DirectionsResult, status: google.maps.DirectionsStatus): void {
+    AjaxStatus.hide();
+
+    if (status === "OK") {
+      DomUtil.show(this.map!.getDiv().parentElement!);
+
+      google.maps.event.trigger(this.map, "resize");
+
+      this.directionsRenderer!.setDirections(result);
+
+      DomUtil.show(this.travelMode!.closest("dl")!);
+      DomUtil.show(this.googleLink!);
+
+      DomUtil.innerError(this.originInput!, false);
+    } else {
+      // map irrelevant errors to not found error
+      if (status !== "OVER_QUERY_LIMIT" && status !== "REQUEST_DENIED") {
+        status = google.maps.DirectionsStatus.NOT_FOUND;
+      }
+
+      DomUtil.innerError(this.originInput!, Language.get(`wcf.map.route.error.${status.toLowerCase()}`));
+    }
+  }
+
+  /**
+   * Updates the route after the travel mode has been changed.
+   */
+  private updateRoute(): void {
+    this._calculateRoute({
+      location: this.lastOrigin!,
+    });
+  }
+
+  /**
+   * Sets up the route planner dialog.
+   */
+  _dialogSetup(): ReturnType<DialogCallbackSetup> {
+    return {
+      id: this.button.id + "Dialog",
+      options: {
+        onShow: this.initDialog.bind(this),
+        title: Language.get("wcf.map.route.planner"),
+      },
+      source: `
+<div class="googleMapsDirectionsContainer" style="display: none;">
+  <div class="googleMap"></div>
+  <div class="googleMapsDirections"></div>
+</div>
+<small class="googleMapsDirectionsGoogleLinkContainer">
+  <a href="${this.getGoogleMapsLink()}" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">${Language.get(
+        "wcf.map.route.viewOnGoogleMaps",
+      )}</a>
+</small>
+<dl>
+  <dt>${Language.get("wcf.map.route.origin")}</dt>
+  <dd>
+    <input type="text" name="origin" class="long" autofocus>
+  </dd>
+</dl>
+<dl style="display: none;">
+  <dt>${Language.get("wcf.map.route.travelMode")}</dt>
+  <dd>
+    <select name="travelMode">
+      <option value="driving">${Language.get("wcf.map.route.travelMode.driving")}</option>
+      <option value="walking">${Language.get("wcf.map.route.travelMode.walking")}</option>
+      <option value="bicycling">${Language.get("wcf.map.route.travelMode.bicycling")}</option>
+      <option value="transit">${Language.get("wcf.map.route.travelMode.transit")}</option>
+    </select>
+  </dd>
+</dl>`,
+    };
+  }
+}
+
+Core.enableLegacyInheritance(ControllerMapRoutePlanner);
+
+export = ControllerMapRoutePlanner;