Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WCF.Location.js
CommitLineData
e368b357
AE
1"use strict";
2
80e131ce
MS
3/**
4 * Location-related classes for WCF
5 *
6 * @author Matthias Schmidt
c839bd49 7 * @copyright 2001-2018 WoltLab GmbH
80e131ce
MS
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
9 */
10WCF.Location = { };
11
12/**
13 * Provides location-related utility functions.
14 */
15WCF.Location.Util = {
16 /**
17 * Passes the user's current latitude and longitude to the given function
18 * as parameters. If the user's current position cannot be determined,
19 * undefined will be passed as both parameters.
20 *
21 * @param function callback
22 * @param integer timeout
23 */
24 getLocation: function(callback, timeout) {
6ae5ec03
MS
25 var $accessUserLocation = WCF.Location.GoogleMaps.Settings.get('accessUserLocation');
26 if (navigator.geolocation && $accessUserLocation !== null && $accessUserLocation) {
80e131ce
MS
27 navigator.geolocation.getCurrentPosition(function(position) {
28 callback(position.coords.latitude, position.coords.longitude);
29 }, function() {
30 callback(undefined, undefined);
31 }, {
32 timeout: timeout || 5000
33 });
34 }
35 else {
36 callback(undefined, undefined);
37 }
38 }
39};
40
41/**
42 * Namespace for Google Maps-related classes.
43 */
44WCF.Location.GoogleMaps = { };
45
513029ce
MS
46/**
47 * After an authentican error, Google Maps tries to call the `gm_authFailure` function.
48 * (see https://developers.google.com/maps/documentation/javascript/events#auth-errors)
49 */
50function gm_authFailure() {
51 WCF.System.Event.fireEvent('com.woltlab.wcf.googleMaps', 'authenticationFailure');
52};
53
80e131ce
MS
54/**
55 * Handles the global Google Maps settings.
56 */
57WCF.Location.GoogleMaps.Settings = {
58 /**
59 * Google Maps settings
60 * @var object
61 */
62 _settings: { },
63
64 /**
65 * Returns the value of a certain setting or null if it doesn't exist.
66 *
67 * If no parameter is given, all settings are returned.
68 *
69 * @param string setting
70 * @return mixed
71 */
72 get: function(setting) {
73 if (setting === undefined) {
74 return this._settings;
75 }
76
77 if (this._settings[setting] !== undefined) {
78 return this._settings[setting];
79 }
80
81 return null;
82 },
83
84 /**
85 * Sets the value of a certain setting.
86 *
87 * @param mixed setting
88 * @param mixed value
89 */
90 set: function(setting, value) {
91 if ($.isPlainObject(setting)) {
92 for (var index in setting) {
93 this._settings[index] = setting[index];
94 }
95 }
96 else {
97 this._settings[setting] = value;
98 }
99 }
100};
101
102/**
103 * Handles a Google Maps map.
104 */
105WCF.Location.GoogleMaps.Map = Class.extend({
106 /**
107 * map object for the displayed map
108 * @var google.maps.Map
109 */
110 _map: null,
111
112 /**
113 * list of markers on the map
114 * @var array<google.maps.Marker>
115 */
116 _markers: [ ],
117
118 /**
1615fc2e 119 * Initializes a new WCF.Location.Map object.
80e131ce
MS
120 *
121 * @param string mapContainerID
122 * @param object mapOptions
123 */
124 init: function(mapContainerID, mapOptions) {
125 this._mapContainer = $('#' + mapContainerID);
126 this._mapOptions = $.extend(true, this._getDefaultMapOptions(), mapOptions);
127
128 this._map = new google.maps.Map(this._mapContainer[0], this._mapOptions);
129 this._markers = [ ];
130
17994b04
MS
131 // fix maps in mobile sidebars by refreshing the map when displaying
132 // the map
133 if (this._mapContainer.parents('.sidebar').length) {
431e4cb4 134 enquire.register('(max-width: 767px)', {
17994b04
MS
135 setup: $.proxy(this._addSidebarMapListener, this),
136 deferSetup: true
137 });
138 }
139
80e131ce
MS
140 this.refresh();
141 },
142
0a2c8393
MS
143 /**
144 * Adds the event listener to a marker to show the associated info window.
145 *
146 * @param google.maps.Marker marker
147 * @param google.maps.InfoWindow infoWindow
148 */
149 _addInfoWindowEventListener: function(marker, infoWindow) {
150 google.maps.event.addListener(marker, 'click', $.proxy(function() {
151 infoWindow.open(this._map, marker);
152 }, this));
153 },
154
17994b04
MS
155 /**
156 * Adds click listener to mobile sidebar toggle button to refresh map.
157 */
158 _addSidebarMapListener: function() {
159 $('.content > .mobileSidebarToggleButton').click($.proxy(this.refresh, this));
160 },
161
80e131ce
MS
162 /**
163 * Returns the default map options.
164 *
165 * @return object
166 */
167 _getDefaultMapOptions: function() {
168 var $defaultMapOptions = { };
169
170 // dummy center value
ceaa4d38 171 $defaultMapOptions.center = new google.maps.LatLng(WCF.Location.GoogleMaps.Settings.get('defaultLatitude'), WCF.Location.GoogleMaps.Settings.get('defaultLongitude'));
80e131ce
MS
172
173 // double click to zoom
174 $defaultMapOptions.disableDoubleClickZoom = WCF.Location.GoogleMaps.Settings.get('disableDoubleClickZoom');
175
176 // draggable
177 $defaultMapOptions.draggable = WCF.Location.GoogleMaps.Settings.get('draggable');
178
179 // map type
180 switch (WCF.Location.GoogleMaps.Settings.get('mapType')) {
181 case 'map':
182 $defaultMapOptions.mapTypeId = google.maps.MapTypeId.ROADMAP;
183 break;
184
185 case 'satellite':
186 $defaultMapOptions.mapTypeId = google.maps.MapTypeId.SATELLITE;
187 break;
188
189 case 'physical':
190 $defaultMapOptions.mapTypeId = google.maps.MapTypeId.TERRAIN;
191 break;
192
193 case 'hybrid':
194 default:
195 $defaultMapOptions.mapTypeId = google.maps.MapTypeId.HYBRID;
196 break;
197 }
198
199 /// map type controls
200 $defaultMapOptions.mapTypeControl = WCF.Location.GoogleMaps.Settings.get('mapTypeControl') != 'off';
201 if ($defaultMapOptions.mapTypeControl) {
202 switch (WCF.Location.GoogleMaps.Settings.get('mapTypeControl')) {
203 case 'dropdown':
204 $defaultMapOptions.mapTypeControlOptions = {
205 style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
206 };
207 break;
208
209 case 'horizontalBar':
210 $defaultMapOptions.mapTypeControlOptions = {
211 style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR
212 };
213 break;
214
215 default:
216 $defaultMapOptions.mapTypeControlOptions = {
217 style: google.maps.MapTypeControlStyle.DEFAULT
218 };
219 break;
220 }
221 }
222
223 // scale control
224 $defaultMapOptions.scaleControl = WCF.Location.GoogleMaps.Settings.get('scaleControl');
225 $defaultMapOptions.scrollwheel = WCF.Location.GoogleMaps.Settings.get('scrollwheel');
226
227 // zoom
228 $defaultMapOptions.zoom = WCF.Location.GoogleMaps.Settings.get('zoom');
229
230 return $defaultMapOptions;
231 },
232
233 /**
234 * Adds a draggable marker at the given position to the map and returns
235 * the created marker object.
236 *
237 * @param float latitude
238 * @param float longitude
239 * @return google.maps.Marker
240 */
241 addDraggableMarker: function(latitude, longitude) {
242 var $marker = new google.maps.Marker({
243 clickable: false,
244 draggable: true,
245 map: this._map,
4c1010d9
MS
246 position: new google.maps.LatLng(latitude, longitude),
247 zIndex: 1
80e131ce
MS
248 });
249
250 this._markers.push($marker);
251
252 return $marker;
253 },
254
255 /**
256 * Adds a marker with the given data to the map and returns the created
257 * marker object.
258 *
259 * @param float latitude
260 * @param float longitude
261 * @param string title
262 * @param mixed icon
263 * @param string information
264 * @return google.maps.Marker
265 */
266 addMarker: function(latitude, longitude, title, icon, information) {
267 var $marker = new google.maps.Marker({
268 map: this._map,
269 position: new google.maps.LatLng(latitude, longitude),
270 title: title
271 });
272
273 // add icon
274 if (icon) {
275 $marker.setIcon(icon);
276 }
277
278 // add info window for marker information
279 if (information) {
280 var $infoWindow = new google.maps.InfoWindow({
281 content: information
282 });
0a2c8393 283 this._addInfoWindowEventListener($marker, $infoWindow);
6cf7dace
MS
284
285 // add info window object to marker object
286 $marker.infoWindow = $infoWindow;
80e131ce
MS
287 }
288
289 this._markers.push($marker);
290
291 return $marker;
292 },
293
294 /**
295 * Returns all markers on the map.
296 *
297 * @return array<google.maps.Marker>
298 */
299 getMarkers: function() {
300 return this._markers;
301 },
302
303 /**
304 * Returns the Google Maps map object.
305 *
306 * @return google.maps.Map
307 */
308 getMap: function() {
309 return this._map;
310 },
311
312 /**
313 * Refreshes the map.
314 */
315 refresh: function() {
f6b34a5b
MS
316 // save current center since resize does not preserve it
317 var $center = this._map.getCenter();
318
80e131ce 319 google.maps.event.trigger(this._map, 'resize');
f6b34a5b
MS
320
321 // set center to old value again
322 this._map.setCenter($center);
80e131ce
MS
323 },
324
325 /**
326 * Refreshes the boundaries of the map to show all markers.
327 */
328 refreshBounds: function() {
329 var $minLatitude = null;
330 var $maxLatitude = null;
331 var $minLongitude = null;
332 var $maxLongitude = null;
333
334 for (var $index in this._markers) {
335 var $marker = this._markers[$index];
336 var $latitude = $marker.getPosition().lat();
337 var $longitude = $marker.getPosition().lng();
338
339 if ($minLatitude === null) {
340 $minLatitude = $maxLatitude = $latitude;
341 $minLongitude = $maxLongitude = $longitude;
342 }
343 else {
344 if ($minLatitude > $latitude) {
345 $minLatitude = $latitude;
346 }
347 else if ($maxLatitude < $latitude) {
348 $maxLatitude = $latitude;
349 }
350
351 if ($minLongitude > $latitude) {
352 $minLongitude = $latitude;
353 }
354 else if ($maxLongitude < $longitude) {
355 $maxLongitude = $longitude;
356 }
357 }
358 }
359
360 this._map.fitBounds(new google.maps.LatLngBounds(
361 new google.maps.LatLng($minLatitude, $minLongitude),
362 new google.maps.LatLng($maxLatitude, $maxLongitude)
363 ));
364 },
365
366 /**
367 * Removes all markers from the map.
368 */
369 removeMarkers: function() {
370 for (var $index in this._markers) {
371 this._markers[$index].setMap(null);
372 }
373
374 this._markers = [ ];
375 },
376
42eb214d
MS
377 /**
378 * Changes the bounds of the map.
379 *
380 * @param object northEast
381 * @param object southWest
382 */
383 setBounds: function(northEast, southWest) {
384 this._map.fitBounds(new google.maps.LatLngBounds(
385 new google.maps.LatLng(southWest.latitude, southWest.longitude),
386 new google.maps.LatLng(northEast.latitude, northEast.longitude)
387 ));
388 },
389
80e131ce
MS
390 /**
391 * Sets the center of the map to the given position.
392 *
393 * @param float latitude
394 * @param float longitude
395 */
396 setCenter: function(latitude, longitude) {
397 this._map.setCenter(new google.maps.LatLng(latitude, longitude));
398 }
399});
400
6cf7dace
MS
401/**
402 * Handles a large map with many markers where (new) markers are loaded via AJAX.
403 */
404WCF.Location.GoogleMaps.LargeMap = WCF.Location.GoogleMaps.Map.extend({
405 /**
406 * name of the PHP class executing the 'getMapMarkers' action
407 * @var string
408 */
409 _actionClassName: null,
410
eaf90fa4
MW
411 /**
412 * additional parameters for executing the 'getMapMarkers' action
413 * @var object
414 */
415 _additionalParameters: { },
416
6cf7dace
MS
417 /**
418 * indicates if the maps center can be set by location search
419 * @var WCF.Location.GoogleMaps.LocationSearch
420 */
421 _locationSearch: null,
422
423 /**
424 * selector for the location search input
425 * @var string
426 */
427 _locationSearchInputSelector: null,
428
429 /**
430 * cluster handling the markers on the map
431 * @var MarkerClusterer
432 */
433 _markerClusterer: null,
434
435 /**
436 * ids of the objects which are already displayed
437 * @var array<integer>
438 */
439 _objectIDs: [ ],
440
441 /**
442 * previous coordinates of the north east map boundary
443 * @var google.maps.LatLng
444 */
445 _previousNorthEast: null,
446
447 /**
448 * previous coordinates of the south west map boundary
449 * @var google.maps.LatLng
450 */
451 _previousSouthWest: null,
452
76d16d7f
MS
453 /**
454 * if `true`, the `exludedObjectIds` array will be sent as a JSON string,
455 * otherwise as an array (default)
456 * note: be prepared that in the future, only JSON strings might be supported
457 * @var boolean
458 */
459 _stringifyExcludedObjectIds: false,
460
6cf7dace
MS
461 /**
462 * @see WCF.Location.GoogleMaps.Map.init()
463 */
eaf90fa4 464 init: function(mapContainerID, mapOptions, actionClassName, locationSearchInputSelector, additionalParameters) {
76d16d7f
MS
465 this._stringifyExcludedObjectIds = false;
466 if (mapOptions && mapOptions.stringifyExcludedObjectIds) {
467 this._stringifyExcludedObjectIds = mapOptions.stringifyExcludedObjectIds;
468 delete mapOptions.stringifyExcludedObjectIds;
469 }
470
6cf7dace
MS
471 this._super(mapContainerID, mapOptions);
472
473 this._actionClassName = actionClassName;
474 this._locationSearchInputSelector = locationSearchInputSelector || '';
eaf90fa4 475 this._additionalParameters = additionalParameters || { };
6cf7dace
MS
476 this._objectIDs = [ ];
477
478 if (this._locationSearchInputSelector) {
479 this._locationSearch = new WCF.Location.GoogleMaps.LocationSearch(locationSearchInputSelector, $.proxy(this._centerMap, this));
480 }
481
f9c4ea7c 482 this._markerClusterer = new MarkerClusterer(this._map, this._markers, {
ab587abc 483 maxZoom: 17,
73c4ffca 484 imagePath: WCF.Location.GoogleMaps.Settings.get('markerClustererImagePath') + 'm'
f9c4ea7c 485 });
6cf7dace 486
0a2c8393
MS
487 this._markerSpiderfier = new OverlappingMarkerSpiderfier(this._map, {
488 keepSpiderfied: true,
489 markersWontHide: true,
490 markersWontMove: true
491 });
492 this._markerSpiderfier.addListener('click', $.proxy(function(marker) {
493 if (marker.infoWindow) {
494 marker.infoWindow.open(this._map, marker);
495 }
496 }, this));
497
6cf7dace
MS
498 this._proxy = new WCF.Action.Proxy({
499 showLoadingOverlay: false,
500 success: $.proxy(this._success, this)
501 });
502
503 this._previousNorthEast = null;
504 this._previousSouthWest = null;
505 google.maps.event.addListener(this._map, 'idle', $.proxy(this._loadMarkers, this));
506 },
507
0a2c8393
MS
508 /**
509 * @see WCF.Location.GoogleMaps.Map.addMarker()
510 */
511 _addInfoWindowEventListener: function(marker, infoWindow) {
512 // does nothing, is handled by the event listener of the marker
513 // spiderfier
514 },
515
6cf7dace
MS
516 /**
517 * Centers the map based on a location search result.
518 *
519 * @param object data
520 */
521 _centerMap: function(data) {
522 this.setCenter(data.location.lat(), data.location.lng());
523
524 $(this._locationSearchInputSelector).val(data.label);
525 },
526
527 /**
2957e71a
MS
528 * Loads markers if the map is reloaded. Returns true if new markers will
529 * be loaded and false if for the current map bounds, the markers have already
530 * been loaded.
531 *
532 * @return boolean
6cf7dace
MS
533 */
534 _loadMarkers: function() {
535 var $northEast = this._map.getBounds().getNorthEast();
536 var $southWest = this._map.getBounds().getSouthWest();
537
538 // check if the user has zoomed in, then all markers are already
539 // displayed
540 if (this._previousNorthEast && this._previousNorthEast.lat() >= $northEast.lat() && this._previousNorthEast.lng() >= $northEast.lng() && this._previousSouthWest.lat() <= $southWest.lat() && this._previousSouthWest.lng() <= $southWest.lng()) {
2957e71a 541 return false;
6cf7dace
MS
542 }
543
544 this._previousNorthEast = $northEast;
545 this._previousSouthWest = $southWest;
546
547 this._proxy.setOption('data', {
548 actionName: 'getMapMarkers',
549 className: this._actionClassName,
eaf90fa4 550 parameters: $.extend(this._additionalParameters, {
76d16d7f 551 excludedObjectIDs: this._stringifyExcludedObjectIds ? JSON.stringify(this._objectIDs) : this._objectIDs,
6cf7dace
MS
552 eastLongitude: $northEast.lng(),
553 northLatitude: $northEast.lat(),
554 southLatitude: $southWest.lat(),
555 westLongitude: $southWest.lng()
eaf90fa4 556 })
6cf7dace
MS
557 });
558 this._proxy.sendRequest();
2957e71a
MS
559
560 return true;
6cf7dace
MS
561 },
562
563 /**
564 * Handles a successful AJAX request.
565 *
566 * @param object data
567 * @param string textStatus
568 * @param jQuery jqXHR
569 */
570 _success: function(data, textStatus, jqXHR) {
571 if (data.returnValues && data.returnValues.markers) {
572 for (var $index in data.returnValues.markers) {
573 var $markerInfo = data.returnValues.markers[$index];
574
575 this.addMarker($markerInfo.latitude, $markerInfo.longitude, $markerInfo.title, null, $markerInfo.infoWindow);
576
577 if ($markerInfo.objectID) {
578 this._objectIDs.push($markerInfo.objectID);
579 }
580 else if ($markerInfo.objectIDs) {
581 this._objectIDs = this._objectIDs.concat($markerInfo.objectIDs);
582 }
583 }
584 }
585 },
586
587 /**
588 * @see WCF.Location.GoogleMaps.Map.addMarker()
589 */
590 addMarker: function(latitude, longitude, title, icon, information) {
591 var $marker = this._super(latitude, longitude, title, icon, information);
592 this._markerClusterer.addMarker($marker);
0a2c8393 593 this._markerSpiderfier.addMarker($marker);
6cf7dace
MS
594
595 return $marker;
596 }
597});
598
2957e71a
MS
599/**
600 * Extends the large map implementation by treating non-draggable markers as location
601 * suggestions.
602 */
603WCF.Location.GoogleMaps.SuggestionMap = WCF.Location.GoogleMaps.LargeMap.extend({
604 /**
605 * maps control showing/hiding location suggestions
606 * @var jQuery
607 */
608 _locationSuggestionsButton: null,
609
610 /**
611 * function called when a location is selected
612 * @var function
613 */
614 _suggestionSelectionCallback: null,
615
616 /**
617 * @see WCF.Location.GoogleMaps.LargeMap.init()
618 */
619 init: function(mapContainerID, mapOptions, actionClassName, locationSearchInputSelector, additionalParameters) {
620 this._super(mapContainerID, mapOptions, actionClassName, locationSearchInputSelector, additionalParameters);
621
1a6e8c52 622 var $locationSuggestionDiv = $('<div class="gmnoprint googleMapsCustomControlContainer"><div class="gm-style-mtc"><div class="googleMapsCustomControl">' + WCF.Language.get('wcf.map.showLocationSuggestions') + '</div></div></div>');
2957e71a
MS
623 this._locationSuggestionsButton = $locationSuggestionDiv.find('.googleMapsCustomControl').click($.proxy(this._toggleLocationSuggestions, this));
624
625 this._map.controls[google.maps.ControlPosition.TOP_RIGHT].push($locationSuggestionDiv.get(0));
626 },
627
628 /**
629 * @see WCF.Location.GoogleMaps.LargeMap._loadMarkers()
630 */
631 _loadMarkers: function() {
632 if (!this._locationSuggestionsButton.hasClass('active')) return;
633
634 if (!this._super()) {
635 this._loadSuggestions = false;
636 }
637 },
638
639 /**
640 * @see WCF.Location.GoogleMaps.LargeMap._loadMarkers()
641 */
642 _success: function(data, textStatus, jqXHR) {
643 var $oldLength = this._markers.length;
644 this._super(data, textStatus, jqXHR);
645
646 if (this._loadSuggestions && $oldLength == this._markers.length) {
647 this._loadSuggestions = false;
648 new WCF.System.Notification(WCF.Language.get('wcf.map.noLocationSuggestions'), 'info').show();
649 }
650 },
651
652 /**
653 * Handles clicks on the location suggestions button.
654 */
655 _toggleLocationSuggestions: function() {
656 var $showSuggestions = !this._locationSuggestionsButton.hasClass('active');
657 if ($showSuggestions) {
658 this._loadSuggestions = true;
659 }
660
661 this.showSuggestions($showSuggestions);
662 },
663
664 /**
665 * @see WCF.Location.GoogleMaps.Map.addMarker()
666 */
667 addMarker: function(latitude, longitude, title, icon, information) {
668 var $infoWindow = $(information);
669 var $useLocation = $('<a class="googleMapsUseLocationSuggestionLink" />').text(WCF.Language.get('wcf.map.useLocationSuggestion')).click(this._suggestionSelectionCallback);
95961bdf 670 $infoWindow.append($('<p />').append($useLocation));
2957e71a
MS
671
672 var $marker = this._super(latitude, longitude, title, '//mt.google.com/vt/icon/name=icons/spotlight/spotlight-waypoint-a.png', $infoWindow.get(0));
673
674 $useLocation.data('marker', $marker);
675
676 return $marker;
677 },
678
679 /**
680 * Sets the function called when a location is selected.
681 *
682 * @param function callback
683 */
684 setSuggestionSelectionCallback: function(callback) {
685 this._suggestionSelectionCallback = callback;
686 },
687
688 /**
689 * Shows or hides the location suggestions.
690 *
691 * @param boolean showSuggestions
692 */
693 showSuggestions: function(showSuggestions) {
694 // missing argument means showing the suggestions
695 if (showSuggestions === undefined) showSuggestions = true;
696
697 this._locationSuggestionsButton.toggleClass('active', showSuggestions);
698
699 var $clusterMarkers = [ ];
700 for (var $i = 0, $length = this._markers.length; $i < $length; $i++) {
701 var $marker = this._markers[$i];
702
703 // ignore draggable markers
704 if (!$marker.draggable) {
705 $marker.setVisible(showSuggestions);
706 if (showSuggestions) {
707 $clusterMarkers.push($marker);
708 }
709 }
710 }
711
712 this._markerClusterer.clearMarkers();
713 if (showSuggestions) {
714 this._markerClusterer.addMarkers($clusterMarkers);
715 }
716
717 this._loadMarkers();
718 }
719});
720
80e131ce
MS
721/**
722 * Provides location searches based on google.maps.Geocoder.
723 */
724WCF.Location.GoogleMaps.LocationSearch = WCF.Search.Base.extend({
725 /**
726 * Google Maps geocoder object
727 * @var google.maps.Geocoder
728 */
729 _geocoder: null,
730
731 /**
732 * @see WCF.Search.Base.init()
733 */
734 init: function(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay) {
735 this._super(searchInput, callback, excludedSearchValues, commaSeperated, showLoadingOverlay);
736
f67fb4e0 737 this.setDelay(500);
80e131ce
MS
738 this._geocoder = new google.maps.Geocoder();
739 },
740
741 /**
742 * @see WCF.Search.Base._createListItem()
743 */
744 _createListItem: function(geocoderResult) {
745 var $listItem = $('<li><span>' + WCF.String.escapeHTML(geocoderResult.formatted_address) + '</span></li>').appendTo(this._list);
746 $listItem.data('location', geocoderResult.geometry.location).data('label', geocoderResult.formatted_address).click($.proxy(this._executeCallback, this));
747
748 this._itemCount++;
749
750 return $listItem;
751 },
752
753 /**
754 * @see WCF.Search.Base._keyUp()
755 */
756 _keyUp: function(event) {
757 // handle arrow keys and return key
758 switch (event.which) {
3e597f7d
AE
759 case $.ui.keyCode.LEFT:
760 case $.ui.keyCode.RIGHT:
80e131ce 761 return;
80e131ce 762
3e597f7d 763 case $.ui.keyCode.UP:
80e131ce
MS
764 this._selectPreviousItem();
765 return;
80e131ce 766
3e597f7d 767 case $.ui.keyCode.DOWN:
80e131ce
MS
768 this._selectNextItem();
769 return;
80e131ce 770
3e597f7d 771 case $.ui.keyCode.ENTER:
80e131ce 772 return this._selectElement(event);
80e131ce
MS
773 }
774
775 var $content = this._getSearchString(event);
776 if ($content === '') {
777 this._clearList(true);
778 }
779 else if ($content.length >= this._triggerLength) {
f67fb4e0
MS
780 if (this._delay) {
781 if (this._timer !== null) {
782 this._timer.stop();
783 }
784
785 this._timer = new WCF.PeriodicalExecuter($.proxy(function() {
786 this._geocoder.geocode({
787 address: $content
788 }, $.proxy(this._success, this));
789
790 this._timer.stop();
791 this._timer = null;
792 }, this), this._delay);
793 }
794 else {
795 this._geocoder.geocode({
796 address: $content
797 }, $.proxy(this._success, this));
798 }
80e131ce
MS
799 }
800 else {
801 // input below trigger length
802 this._clearList(false);
803 }
804 },
805
806 /**
1615fc2e 807 * Handles a successful geocoder request.
80e131ce
MS
808 *
809 * @param array results
810 * @param integer status
811 */
812 _success: function(results, status) {
cb34315a
MS
813 this._clearList(false);
814
80e131ce
MS
815 if (status != google.maps.GeocoderStatus.OK) {
816 return;
817 }
818
819 if ($.getLength(results)) {
820 var $count = 0;
821 for (var $index in results) {
822 this._createListItem(results[$index]);
823
824 if (++$count == 10) {
825 break;
826 }
827 }
828 }
829 else if (!this._handleEmptyResult()) {
830 return;
831 }
832
833 WCF.CloseOverlayHandler.addCallback('WCF.Search.Base', $.proxy(function() { this._clearList(); }, this));
834
835 var $containerID = this._searchInput.parents('.dropdown').wcfIdentify();
836 if (!WCF.Dropdown.getDropdownMenu($containerID).hasClass('dropdownOpen')) {
837 WCF.Dropdown.toggleDropdown($containerID);
838 }
839
840 // pre-select first item
841 this._itemIndex = -1;
842 if (!WCF.Dropdown.getDropdown($containerID).data('disableAutoFocus')) {
843 this._selectNextItem();
844 }
845 }
846});
847
848/**
849 * Handles setting a single location on a Google Map.
850 */
851WCF.Location.GoogleMaps.LocationInput = Class.extend({
852 /**
853 * location search object
854 * @var WCF.Location.GoogleMaps.LocationSearch
855 */
856 _locationSearch: null,
857
858 /**
859 * related map object
860 * @var WCF.Location.GoogleMaps.Map
861 */
862 _map: null,
863
864 /**
865 * draggable marker to set the location
866 * @var google.maps.Marker
867 */
868 _marker: null,
869
870 /**
871 * Initializes a new WCF.Location.GoogleMaps.LocationInput object.
872 *
873 * @param string mapContainerID
874 * @param object mapOptions
875 * @param string searchInput
73f5239b
MS
876 * @param float latitude
877 * @param float longitude
2957e71a 878 * @param string actionClassName
80e131ce 879 */
2957e71a 880 init: function(mapContainerID, mapOptions, searchInput, latitude, longitude, actionClassName) {
80e131ce 881 this._searchInput = searchInput;
2957e71a
MS
882
883 if (actionClassName) {
884 this._map = new WCF.Location.GoogleMaps.SuggestionMap(mapContainerID, mapOptions, actionClassName);
885 this._map.setSuggestionSelectionCallback($.proxy(this._useSuggestion, this));
886 }
887 else {
888 this._map = new WCF.Location.GoogleMaps.Map(mapContainerID, mapOptions);
889 }
890
80e131ce
MS
891 this._locationSearch = new WCF.Location.GoogleMaps.LocationSearch(searchInput, $.proxy(this._setMarkerByLocation, this));
892
893 if (latitude && longitude) {
894 this._marker = this._map.addDraggableMarker(latitude, longitude);
895 }
896 else {
d6693d81 897 this._marker = this._map.addDraggableMarker(WCF.Location.GoogleMaps.Settings.get('defaultLatitude'), WCF.Location.GoogleMaps.Settings.get('defaultLongitude'));
80e131ce
MS
898
899 WCF.Location.Util.getLocation($.proxy(function(latitude, longitude) {
2cf378ae
MS
900 if (latitude !== undefined && longitude !== undefined) {
901 WCF.Location.GoogleMaps.Util.moveMarker(this._marker, latitude, longitude);
fba1ceef 902 WCF.Location.GoogleMaps.Util.focusMarker(this._marker);
2cf378ae 903 }
80e131ce
MS
904 }, this));
905 }
bcf17845
AE
906
907 this._marker.addListener('dragend', $.proxy(this._updateLocation, this));
80e131ce
MS
908 },
909
910 /**
2957e71a
MS
911 * Uses a suggestion by clicking on the "Use suggestion" link in the marker's
912 * info window.
80e131ce 913 *
2957e71a 914 * @param Event event
80e131ce 915 */
2957e71a
MS
916 _useSuggestion: function(event) {
917 var $marker = $(event.currentTarget).data('marker');
918
919 this._marker.setPosition($marker.getPosition());
920 this._updateLocation();
921
922 // hide suggestions
923 this._map.showSuggestions(false);
80e131ce
MS
924 },
925
bcf17845
AE
926 /**
927 * Updates location on marker position change.
928 */
929 _updateLocation: function() {
930 WCF.Location.GoogleMaps.Util.reverseGeocoding($.proxy(function(result) {
931 if (result !== null) {
932 $(this._searchInput).val(result);
933 }
934 }, this), this._marker);
935 },
936
80e131ce
MS
937 /**
938 * Sets the marker based on an entered location.
939 *
940 * @param object data
941 */
942 _setMarkerByLocation: function(data) {
943 this._marker.setPosition(data.location);
944 WCF.Location.GoogleMaps.Util.focusMarker(this._marker);
945
3e597f7d 946 $(this._searchInput).val(data.label);
2957e71a
MS
947 },
948
949 /**
950 * Returns the related map.
951 *
952 * @return WCF.Location.GoogleMaps.Map
953 */
954 getMap: function() {
955 return this._map;
956 },
957
958 /**
959 * Returns the draggable marker used to set the location.
960 *
961 * @return google.maps.Marker
962 */
963 getMarker: function() {
964 return this._marker;
80e131ce
MS
965 }
966});
967
968/**
969 * Provides utility functions for Google Maps maps.
970 */
971WCF.Location.GoogleMaps.Util = {
7013ec88
AE
972 /**
973 * geocoder instance
974 * @var google.maps.Geocoder
975 */
976 _geocoder: null,
977
80e131ce
MS
978 /**
979 * Focuses the given marker's map on the marker.
980 *
981 * @param google.maps.Marker marker
982 */
983 focusMarker: function(marker) {
984 marker.getMap().setCenter(marker.getPosition());
985 },
986
987 /**
988 * Returns the latitude and longitude of the given marker.
989 *
990 * @return object
991 */
992 getMarkerPosition: function(marker) {
993 return {
994 latitude: marker.getPosition().lat(),
995 longitude: marker.getPosition().lng()
996 };
997 },
998
999 /**
1000 * Moves the given marker to the given position.
1001 *
1002 * @param google.maps.Marker marker
1003 * @param float latitude
1004 * @param float longitude
ab6d3626 1005 * @param boolean dragend indicates if "dragend" event is fired
80e131ce 1006 */
ab6d3626 1007 moveMarker: function(marker, latitude, longitude, triggerDragend) {
80e131ce 1008 marker.setPosition(new google.maps.LatLng(latitude, longitude));
ab6d3626
MS
1009
1010 if (triggerDragend) {
1011 google.maps.event.trigger(marker, 'dragend');
1012 }
7013ec88
AE
1013 },
1014
1015 /**
1016 * Performs a reverse geocoding request.
1017 *
1018 * @param object callback
1019 * @param google.maps.Marker marker
1020 * @param string latitude
1021 * @param string longitude
1022 * @param boolean fullResult
1023 */
1024 reverseGeocoding: function(callback, marker, latitude, longitude, fullResult) {
1025 if (marker) {
1026 latitude = marker.getPosition().lat();
1027 longitude = marker.getPosition().lng();
1028 }
1029
1030 if (this._geocoder === null) {
1031 this._geocoder = new google.maps.Geocoder();
1032 }
1033
1034 var $latLng = new google.maps.LatLng(latitude, longitude);
1035 this._geocoder.geocode({ latLng: $latLng }, function(results, status) {
1036 if (status == google.maps.GeocoderStatus.OK) {
1037 callback((fullResult ? results : results[0].formatted_address));
1038 }
1039 else {
1040 callback(null);
1041 }
1042 });
2cf378ae 1043 }
80e131ce 1044};