Commit | Line | Data |
---|---|---|
867c4493 AE |
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 | |
93c0845a | 8 | * @woltlabExcludeBundle all |
867c4493 AE |
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(); | |
5ffe7716 TD |
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)); | |
867c4493 AE |
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 | */ | |
5ffe7716 | 141 | private setRoute(result: google.maps.DirectionsResult | null, status: google.maps.DirectionsStatus): void { |
867c4493 AE |
142 | AjaxStatus.hide(); |
143 | ||
6b64df9d | 144 | if (status === google.maps.DirectionsStatus.OK) { |
867c4493 AE |
145 | DomUtil.show(this.map!.getDiv().parentElement!); |
146 | ||
5ffe7716 | 147 | google.maps.event.trigger(this.map!, "resize"); |
867c4493 AE |
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 | |
6b64df9d TD |
157 | if ( |
158 | status !== google.maps.DirectionsStatus.OVER_QUERY_LIMIT && | |
159 | status !== google.maps.DirectionsStatus.REQUEST_DENIED | |
160 | ) { | |
867c4493 AE |
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; |