Satisfy ESLint
[GitHub/WoltLab/WCF.git] / ts / WoltLabSuite / Core / Controller / Map / Route / Planner.ts
1 /**
2 * Map route planner based on Google Maps.
3 *
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
9 */
10
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";
17
18 interface LocationData {
19 label?: string;
20 location: google.maps.LatLng;
21 }
22
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;
34
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}'`);
39 }
40 this.button = button;
41
42 this.button.addEventListener("click", (ev) => this.openDialog(ev));
43
44 this.destination = destination;
45 }
46
47 /**
48 * Calculates the route based on the given result of a location search.
49 */
50 _calculateRoute(data: LocationData): void {
51 const dialog = UiDialog.getDialog(this)!.dialog;
52
53 if (data.label) {
54 this.originInput!.value = data.label;
55 }
56
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"),
65 });
66
67 this.directionsService = new google.maps.DirectionsService();
68 this.directionsRenderer = new google.maps.DirectionsRenderer();
69
70 this.directionsRenderer.setMap(this.map);
71 const directionsContainer = dialog.querySelector(".googleMapsDirections") as HTMLElement;
72 this.directionsRenderer.setPanel(directionsContainer);
73
74 this.googleLink = dialog.querySelector(".googleMapsDirectionsGoogleLink") as HTMLAnchorElement;
75 }
76
77 const request = {
78 destination: this.destination,
79 origin: data.location,
80 provideRouteAlternatives: true,
81 travelMode: google.maps.TravelMode[this.travelMode!.value.toUpperCase()],
82 };
83
84 AjaxStatus.show();
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));
87
88 this.googleLink!.href = this.getGoogleMapsLink(data.location, this.travelMode!.value);
89
90 this.lastOrigin = data.location;
91 }
92
93 /**
94 * Returns the Google Maps link based on the given optional directions origin
95 * and optional travel mode.
96 */
97 private getGoogleMapsLink(origin?: google.maps.LatLng, travelMode?: string): string {
98 if (origin) {
99 let link = `https://www.google.com/maps/dir/?api=1&origin=${origin.lat()},${origin.lng()}&destination=${this.destination.lat()},${this.destination.lng()}`;
100
101 if (travelMode) {
102 link += `&travelmode=${travelMode}`;
103 }
104
105 return link;
106 }
107
108 return `https://www.google.com/maps/search/?api=1&query=${this.destination.lat()},${this.destination.lng()}`;
109 }
110
111 /**
112 * Initializes the route planning dialog.
113 */
114 private initDialog(): void {
115 if (!this.didInitDialog) {
116 const dialog = UiDialog.getDialog(this)!.dialog;
117
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));
121
122 this.travelMode = dialog.querySelector('select[name="travelMode"]') as HTMLSelectElement;
123 this.travelMode.addEventListener("change", this.updateRoute.bind(this));
124
125 this.didInitDialog = true;
126 }
127 }
128
129 /**
130 * Opens the route planning dialog.
131 */
132 private openDialog(event: Event): void {
133 event.preventDefault();
134
135 UiDialog.open(this);
136 }
137
138 /**
139 * Handles the response of the direction service.
140 */
141 private setRoute(result: google.maps.DirectionsResult | null, status: google.maps.DirectionsStatus): void {
142 AjaxStatus.hide();
143
144 if (status === google.maps.DirectionsStatus.OK) {
145 DomUtil.show(this.map!.getDiv().parentElement!);
146
147 google.maps.event.trigger(this.map!, "resize");
148
149 this.directionsRenderer!.setDirections(result);
150
151 DomUtil.show(this.travelMode!.closest("dl")!);
152 DomUtil.show(this.googleLink!);
153
154 DomUtil.innerError(this.originInput!, false);
155 } else {
156 // map irrelevant errors to not found error
157 if (
158 status !== google.maps.DirectionsStatus.OVER_QUERY_LIMIT &&
159 status !== google.maps.DirectionsStatus.REQUEST_DENIED
160 ) {
161 status = google.maps.DirectionsStatus.NOT_FOUND;
162 }
163
164 DomUtil.innerError(this.originInput!, Language.get(`wcf.map.route.error.${status.toLowerCase()}`));
165 }
166 }
167
168 /**
169 * Updates the route after the travel mode has been changed.
170 */
171 private updateRoute(): void {
172 this._calculateRoute({
173 location: this.lastOrigin!,
174 });
175 }
176
177 /**
178 * Sets up the route planner dialog.
179 */
180 _dialogSetup(): ReturnType<DialogCallbackSetup> {
181 return {
182 id: this.button.id + "Dialog",
183 options: {
184 onShow: this.initDialog.bind(this),
185 title: Language.get("wcf.map.route.planner"),
186 },
187 source: `
188 <div class="googleMapsDirectionsContainer" style="display: none;">
189 <div class="googleMap"></div>
190 <div class="googleMapsDirections"></div>
191 </div>
192 <small class="googleMapsDirectionsGoogleLinkContainer">
193 <a href="${this.getGoogleMapsLink()}" class="googleMapsDirectionsGoogleLink" target="_blank" style="display: none;">${Language.get(
194 "wcf.map.route.viewOnGoogleMaps",
195 )}</a>
196 </small>
197 <dl>
198 <dt>${Language.get("wcf.map.route.origin")}</dt>
199 <dd>
200 <input type="text" name="origin" class="long" autofocus>
201 </dd>
202 </dl>
203 <dl style="display: none;">
204 <dt>${Language.get("wcf.map.route.travelMode")}</dt>
205 <dd>
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>
211 </select>
212 </dd>
213 </dl>`,
214 };
215 }
216 }
217
218 Core.enableLegacyInheritance(ControllerMapRoutePlanner);
219
220 export = ControllerMapRoutePlanner;