2 * Map route planner based on Google Maps.
4 * @author Matthias Schmidt
5 * @copyright 2001-2019 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Controller/Map/Route/Planner
8 * @woltlabExcludeBundle all
11 import * as AjaxStatus from "../../../Ajax/Status";
12 import * as Core from "../../../Core";
13 import DomUtil from "../../../Dom/Util";
14 import * as Language from "../../../Language";
15 import UiDialog from "../../../Ui/Dialog";
16 import { DialogCallbackObject, DialogCallbackSetup } from "../../../Ui/Dialog/Data";
18 interface LocationData {
20 location: google.maps.LatLng;
23 class ControllerMapRoutePlanner implements DialogCallbackObject {
24 private readonly button: HTMLElement;
25 private readonly destination: google.maps.LatLng;
26 private didInitDialog = false;
27 private directionsRenderer?: google.maps.DirectionsRenderer = undefined;
28 private directionsService?: google.maps.DirectionsService = undefined;
29 private googleLink?: HTMLAnchorElement = undefined;
30 private lastOrigin?: google.maps.LatLng = undefined;
31 private map?: google.maps.Map = undefined;
32 private originInput?: HTMLInputElement = undefined;
33 private travelMode?: HTMLSelectElement = undefined;
35 constructor(buttonId: string, destination: google.maps.LatLng) {
36 const button = document.getElementById(buttonId);
37 if (button === null) {
38 throw new Error(`Unknown button with id '${buttonId}'`);
42 this.button.addEventListener("click", (ev) => this.openDialog(ev));
44 this.destination = destination;
48 * Calculates the route based on the given result of a location search.
50 _calculateRoute(data: LocationData): void {
51 const dialog = UiDialog.getDialog(this)!.dialog;
54 this.originInput!.value = data.label;
57 if (this.map === undefined) {
58 const mapContainer = dialog.querySelector(".googleMap") as HTMLElement;
59 this.map = new google.maps.Map(mapContainer, {
60 disableDoubleClickZoom: window.WCF.Location.GoogleMaps.Settings.get("disableDoubleClickZoom"),
61 draggable: window.WCF.Location.GoogleMaps.Settings.get("draggable"),
62 mapTypeId: google.maps.MapTypeId.ROADMAP,
63 scaleControl: window.WCF.Location.GoogleMaps.Settings.get("scaleControl"),
64 scrollwheel: window.WCF.Location.GoogleMaps.Settings.get("scrollwheel"),
67 this.directionsService = new google.maps.DirectionsService();
68 this.directionsRenderer = new google.maps.DirectionsRenderer();
70 this.directionsRenderer.setMap(this.map);
71 const directionsContainer = dialog.querySelector(".googleMapsDirections") as HTMLElement;
72 this.directionsRenderer.setPanel(directionsContainer);
74 this.googleLink = dialog.querySelector(".googleMapsDirectionsGoogleLink") as HTMLAnchorElement;
78 destination: this.destination,
79 origin: data.location,
80 provideRouteAlternatives: true,
81 travelMode: google.maps.TravelMode[this.travelMode!.value.toUpperCase()],
85 // .route() returns a promise, but we rely on the callback API for compatibility reasons.
86 void this.directionsService!.route(request, (result, status) => this.setRoute(result, status));
88 this.googleLink!.href = this.getGoogleMapsLink(data.location, this.travelMode!.value);
90 this.lastOrigin = data.location;
94 * Returns the Google Maps link based on the given optional directions origin
95 * and optional travel mode.
97 private getGoogleMapsLink(origin?: google.maps.LatLng, travelMode?: string): string {
99 let link = `https://www.google.com/maps/dir/?api=1&origin=${origin.lat()},${origin.lng()}&destination=${this.destination.lat()},${this.destination.lng()}`;
102 link += `&travelmode=${travelMode}`;
108 return `https://www.google.com/maps/search/?api=1&query=${this.destination.lat()},${this.destination.lng()}`;
112 * Initializes the route planning dialog.
114 private initDialog(): void {
115 if (!this.didInitDialog) {
116 const dialog = UiDialog.getDialog(this)!.dialog;
118 // make input element a location search
119 this.originInput = dialog.querySelector('input[name="origin"]') as HTMLInputElement;
120 new window.WCF.Location.GoogleMaps.LocationSearch(this.originInput, (data) => this._calculateRoute(data));
122 this.travelMode = dialog.querySelector('select[name="travelMode"]') as HTMLSelectElement;
123 this.travelMode.addEventListener("change", this.updateRoute.bind(this));
125 this.didInitDialog = true;
130 * Opens the route planning dialog.
132 private openDialog(event: Event): void {
133 event.preventDefault();
139 * Handles the response of the direction service.
141 private setRoute(result: google.maps.DirectionsResult | null, status: google.maps.DirectionsStatus): void {
144 if (status === google.maps.DirectionsStatus.OK) {
145 DomUtil.show(this.map!.getDiv().parentElement!);
147 google.maps.event.trigger(this.map!, "resize");
149 this.directionsRenderer!.setDirections(result);
151 DomUtil.show(this.travelMode!.closest("dl")!);
152 DomUtil.show(this.googleLink!);
154 DomUtil.innerError(this.originInput!, false);
156 // map irrelevant errors to not found error
158 status !== google.maps.DirectionsStatus.OVER_QUERY_LIMIT &&
159 status !== google.maps.DirectionsStatus.REQUEST_DENIED
161 status = google.maps.DirectionsStatus.NOT_FOUND;
164 DomUtil.innerError(this.originInput!, Language.get(`wcf.map.route.error.${status.toLowerCase()}`));
169 * Updates the route after the travel mode has been changed.
171 private updateRoute(): void {
172 this._calculateRoute({
173 location: this.lastOrigin!,
178 * Sets up the route planner dialog.
180 _dialogSetup(): ReturnType<DialogCallbackSetup> {
182 id: this.button.id + "Dialog",
184 onShow: this.initDialog.bind(this),
185 title: Language.get("wcf.map.route.planner"),
188 <div class="googleMapsDirectionsContainer" style="display: none;">
189 <div class="googleMap"></div>
190 <div class="googleMapsDirections"></div>
192 <small class="googleMapsDirectionsGoogleLinkContainer">
193 <a href="${this.getGoogleMapsLink()}" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">${Language.get(
194 "wcf.map.route.viewOnGoogleMaps",
198 <dt>${Language.get("wcf.map.route.origin")}</dt>
200 <input type="text" name="origin" class="long" autofocus>
203 <dl style="display: none;">
204 <dt>${Language.get("wcf.map.route.travelMode")}</dt>
206 <select name="travelMode">
207 <option value="driving">${Language.get("wcf.map.route.travelMode.driving")}</option>
208 <option value="walking">${Language.get("wcf.map.route.travelMode.walking")}</option>
209 <option value="bicycling">${Language.get("wcf.map.route.travelMode.bicycling")}</option>
210 <option value="transit">${Language.get("wcf.map.route.travelMode.transit")}</option>
218 Core.enableLegacyInheritance(ControllerMapRoutePlanner);
220 export = ControllerMapRoutePlanner;