Skip to content

Instantly share code, notes, and snippets.

@markdboyd
Created June 7, 2017 22:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markdboyd/6f53fb743f1b1e0fbe9e9530c96e2073 to your computer and use it in GitHub Desktop.
Save markdboyd/6f53fb743f1b1e0fbe9e9530c96e2073 to your computer and use it in GitHub Desktop.
diff --git a/geolocation.libraries.yml b/geolocation.libraries.yml
index 71c2fcf..b5dfaaf 100644
--- a/geolocation.libraries.yml
+++ b/geolocation.libraries.yml
@@ -22,6 +22,12 @@ geolocation.views.filter.geocoder:
dependencies:
- geolocation/geolocation.geocoder
+# HTML5 Geolocation API.
+geolocation.html5:
+ version: 1.x
+ js:
+ js/geolocation-html5.js: { scope: footer }
+
# HTML5 widget library.
geolocation.widgets.html5:
version: 1.x
@@ -31,6 +37,17 @@ geolocation.widgets.html5:
js:
js/geolocation-widget-html5.js: { scope: footer }
+# HTML5 proximity field.
+geolocation.proximity.html5:
+ version: 1.x
+ js:
+ js/geolocation-proximity-html5.js: { scope: footer }
+ dependencies:
+ - core/drupal.ajax
+ - views/views.ajax
+ - geolocation/geolocation.googlemapsapi
+ - geolocation/geolocation.html5
+
#
#
# Google Maps API
diff --git a/js/geolocation-common-map.js b/js/geolocation-common-map.js
index 0b14d3f..d86798f 100644
--- a/js/geolocation-common-map.js
+++ b/js/geolocation-common-map.js
@@ -373,7 +373,16 @@
};
if (typeof location.data('icon') !== 'undefined') {
- markerConfig.icon = location.data('icon');
+ if (location.data('labelOriginX') !== 'undefined' &&
+ location.data('labelOriginY') !== 'undefined') {
+ markerConfig.icon = {
+ url: location.data('icon'),
+ labelOrigin: new google.maps.Point(location.data('labelOriginX'),location.data('labelOriginY'))
+ };
+ }
+ else {
+ markerConfig.icon = location.data('icon');
+ }
}
if (typeof location.data('markerLabel') !== 'undefined') {
@@ -495,7 +504,11 @@
markerClustererStyles = commonMapSettings.markerClusterer.styles;
}
- new MarkerClusterer(
+ if (geolocationMap.markerClusterer) {
+ geolocationMap.markerClusterer.clearMarkers();
+ }
+
+ geolocationMap.markerClusterer = new MarkerClusterer(
geolocationMap.googleMap,
geolocationMap.mapMarkers,
{
diff --git a/js/geolocation-google-maps-api.js b/js/geolocation-google-maps-api.js
index 69e0be4..a63d66a 100644
--- a/js/geolocation-google-maps-api.js
+++ b/js/geolocation-google-maps-api.js
@@ -434,7 +434,6 @@
*/
Drupal.geolocation.removeMapMarker = function (map) {
map.mapMarkers = map.mapMarkers || [];
-
$.each(
map.mapMarkers,
diff --git a/js/geolocation-html5.js b/js/geolocation-html5.js
new file mode 100644
index 0000000..b9fdf67
--- /dev/null
+++ b/js/geolocation-html5.js
@@ -0,0 +1,106 @@
+/**
+ * @file
+ * Javascript for integrating the W3C Geolocation API.
+ */
+
+(function ($, Drupal, drupalSettings, navigator) {
+
+ 'use strict';
+
+ /**
+ * @namespace
+ */
+ Drupal.geolocation = Drupal.geolocation || {};
+ Drupal.geolocation.html5 = Drupal.geolocation.html5 || {};
+
+ drupalSettings.geolocation.html5 = drupalSettings.geolocation.html5 || {};
+
+ /**
+ * Adds a callback that will be called when client location is retrieved.
+ *
+ * @callback {geolocationHTML5ResultCallback} callback - The callback
+ */
+ Drupal.geolocation.html5.addResultCallback = function (callback) {
+ Drupal.geolocation.html5.resultCallbacks = Drupal.geolocation.html5.resultCallbacks || [];
+ Drupal.geolocation.html5.resultCallbacks.push({callback: callback});
+ };
+
+ /**
+ * Get the client location using HTML5 Geolocation API.
+ *
+ * @callback success - The success callback
+ * @callback error - The error callback
+ * @param {object} options - The options for the HTML5 geolocation
+ */
+ Drupal.geolocation.html5.getClientLocation = function(success, error, options) {
+ if (typeof success === 'undefined' || !success) {
+ alert(Drupal.t('You must provide a success handler for the W3C Geolocation API.'));
+ return;
+ }
+
+ var error = (error && typeof error === 'function') ? error : Drupal.geolocation.html5.getClientLocationError;
+ var options = options || {
+ enableHighAccuracy: true,
+ timeout: 5000,
+ maximumAge: 6000
+ };
+
+ // If the browser supports W3C Geolocation API.
+ if (navigator.geolocation) {
+ // Get the geolocation from the browser.
+ navigator.geolocation.getCurrentPosition(success, error, options);
+ }
+ else {
+ alert(Drupal.t('No location data found. Your browser does not support the W3C Geolocation API.'));
+ }
+ };
+
+ /**
+ * Default error handler for Geolocation API.
+ *
+ * @param {object} error - The error object.
+ */
+ Drupal.geolocation.html5.getClientLocationError = function(error) {
+ // Alert with error message.
+ switch (error.code) {
+ case error.PERMISSION_DENIED:
+ console.log(Drupal.t('No location data found. Reason: PERMISSION_DENIED.'));
+ drupalSettings.geolocation.html5.permissionDenied = true;
+ break;
+ case error.POSITION_UNAVAILABLE:
+ console.log(Drupal.t('No location data found. Reason: POSITION_UNAVAILABLE.'));
+ break;
+ case error.TIMEOUT:
+ console.log(Drupal.t('No location data found. Reason: TIMEOUT.'));
+ break;
+ default:
+ console.log(Drupal.t('No location data found. Reason: Unknown error.'));
+ break;
+ }
+ };
+
+ /**
+ * Attach HTML5 geolocation functionality.
+ *
+ * @type {Drupal~behavior}
+ */
+ Drupal.behaviors.geolocationHTML5 = {
+ attach: function (context) {
+ if (!drupalSettings.geolocation.html5.retrieved &&
+ !drupalSettings.geolocation.html5.permissionDenied) {
+ // Get the client location using the Geolocation API.
+ Drupal.geolocation.html5.getClientLocation(function(position) {
+ // Save the state of client location retrieval.
+ drupalSettings.geolocation.html5.retrieved = true;
+
+ // Iterate over callbacks and call them with the geolocation result.
+ Drupal.geolocation.html5.resultCallbacks = Drupal.geolocation.html5.resultCallbacks || [];
+ $.each(Drupal.geolocation.html5.resultCallbacks, function (index, callbackContainer) {
+ callbackContainer.callback(position);
+ });
+ });
+ }
+ },
+ };
+
+})(jQuery, Drupal, drupalSettings, navigator);
diff --git a/js/geolocation-proximity-html5.js b/js/geolocation-proximity-html5.js
new file mode 100644
index 0000000..c8b5323
--- /dev/null
+++ b/js/geolocation-proximity-html5.js
@@ -0,0 +1,67 @@
+(function ($, Drupal, drupalSettings) {
+ 'use strict';
+
+ /**
+ * @namespace
+ */
+ drupalSettings.geolocation.html5 = drupalSettings.geolocation.html5 || {};
+ drupalSettings.geolocation.html5.proximity_view_ids = drupalSettings.geolocation.html5.proximity_view_ids || [];
+
+ Drupal.geolocation = Drupal.geolocation || {};
+ Drupal.geolocation.proximityHTML5 = Drupal.geolocation.proximityHTML5 || {};
+
+ Drupal.geolocation.proximityHTML5.refreshView = function(mapId, map, coordinates, context) {
+ // Get the AJAX settings for this view.
+ var viewSettings = Drupal.views.instances['views_dom_id:' + mapId];
+ var geolocationAjaxSettings = viewSettings.element_settings;
+
+ // Change the progress indicator.
+ geolocationAjaxSettings.progress.type = 'throbber';
+
+ // Add the coordinates to the data to be submitted with the
+ // request.
+ geolocationAjaxSettings.submit['proximity_lat'] = coordinates.latitude;
+ geolocationAjaxSettings.submit['proximity_lng'] = coordinates.longitude;
+
+ // Use AJAX to refresh the view.
+ $('.js-view-dom-id-' + mapId, context).trigger('RefreshView');
+ };
+
+ Drupal.behaviors.geolocationProximityHTML5 = {
+ attach: function(context) {
+ if (!drupalSettings.geolocation.html5.proximity_view_ids.length ||
+ drupalSettings.geolocation.html5.permissionDenied ||
+ drupalSettings.geolocation.html5.hasCoordinates) {
+ return;
+ }
+
+ $.each(drupalSettings.geolocation.html5.proximity_view_ids, function(key, dom_id) {
+ Drupal.geolocation.proximityHTML5[dom_id] = Drupal.geolocation.proximityHTML5[dom_id] || {};
+ Drupal.geolocation.proximityHTML5[dom_id].mapLoaded = $.Deferred();
+ Drupal.geolocation.proximityHTML5[dom_id].receivedCoordinates = $.Deferred();
+
+ Drupal.geolocation.addMapLoadedCallback(function(map) {
+ Drupal.geolocation.proximityHTML5[dom_id].mapLoaded.resolve(map);
+ }, dom_id);
+
+ Drupal.geolocation.html5.addResultCallback(function (position) {
+ // If specified, auto refresh the view with the received
+ // HTML5 coordinates.
+ if (drupalSettings.geolocation.html5[dom_id] &&
+ drupalSettings.geolocation.html5[dom_id].auto_refresh) {
+ Drupal.geolocation.proximityHTML5[dom_id].receivedCoordinates.resolve(position);
+ }
+ });
+
+ // Wait for promises to be resolved for the map to be loaded and
+ // coordinates to be received before acting on the map.
+ $.when(
+ Drupal.geolocation.proximityHTML5[dom_id].mapLoaded,
+ Drupal.geolocation.proximityHTML5[dom_id].receivedCoordinates
+ ).then(function(map, position) {
+ Drupal.geolocation.proximityHTML5.refreshView(dom_id, map, position.coords, context);
+ });
+ });
+ }
+ };
+})(jQuery, Drupal, drupalSettings);
\ No newline at end of file
diff --git a/js/geolocation-widget-html5.js b/js/geolocation-widget-html5.js
index 32f3272..42442d3 100644
--- a/js/geolocation-widget-html5.js
+++ b/js/geolocation-widget-html5.js
@@ -15,7 +15,7 @@
* @prop {Drupal~behaviorAttach} attach
* Attaches html5 widget functionality to relevant elements.
*/
- Drupal.behaviors.geolocationHTML5 = {
+ Drupal.behaviors.geolocationWidgetHTML5 = {
attach: function (context, settings) {
$('.geolocation-html5-button:not(.disabled)').each(function (index) {
// The parent element.
diff --git a/src/Plugin/views/field/ProximityField.php b/src/Plugin/views/field/ProximityField.php
index d473cf7..526ef2e 100644
--- a/src/Plugin/views/field/ProximityField.php
+++ b/src/Plugin/views/field/ProximityField.php
@@ -3,6 +3,7 @@
namespace Drupal\geolocation\Plugin\views\field;
use Drupal\geolocation\GeolocationCore;
+use Drupal\views\Plugin\views\exposed_form\InputRequired;
use Drupal\views\ResultRow;
use Drupal\views\Plugin\views\field\NumericField;
use Drupal\Core\Render\Element;
@@ -11,7 +12,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
- * Field handler for geolocaiton field.
+ * Field handler for geolocation field.
*
* @ingroup views_field_handlers
*
@@ -102,6 +103,7 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
'#options' => [
'direct_input' => $this->t('Static Values'),
'user_input' => $this->t('User input'),
+ 'client_location' => $this->t('Client location (HTML5)'),
],
];
@@ -154,6 +156,8 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
['select[name="options[proximity_source]"]' => ['value' => 'entity_id_argument']],
'or',
['select[name="options[proximity_source]"]' => ['value' => 'user_input']],
+ 'or',
+ ['select[name="options[proximity_source]"]' => ['value' => 'client_location']],
],
],
],
@@ -378,6 +382,21 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
$units = $this->options['proximity_units'];
break;
+ case 'client_location':
+ $geolocation = $this->getStoredUserGeolocation();
+ if ($geolocation) {
+ list($latitude, $longitude) = explode(':', $geolocation);
+ }
+ else {
+ $latitude = $this->view->getRequest()->get('proximity_lat', '');
+ $longitude = $this->view->getRequest()->get('proximity_lng', '');
+ // Set the geolocation coordinates in a cookie so subsequent requests
+ // don't have to retrieve the geolocation from the client.
+ $this->storeUserGeolocation($latitude, $longitude);
+ }
+ $units = $this->options['proximity_units'];
+ break;
+
case 'filter':
/** @var \Drupal\geolocation\Plugin\views\filter\ProximityFilter $filter */
$filter = $this->view->filter[$this->options['proximity_filter']];
@@ -473,29 +492,35 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
* The current state of the form.
*/
public function viewsForm(array &$form, FormStateInterface $form_state) {
- if ($this->options['proximity_source'] != 'user_input') {
+ if (!in_array($this->options['proximity_source'], ['user_input', 'client_location'])) {
unset($form['actions']);
return;
}
+
+ $proximity_source = $this->options['proximity_source'];
+
$form['#cache']['max-age'] = 0;
$form['#method'] = 'GET';
$form['#attributes']['class'][] = 'geolocation-views-proximity-field';
+ $proximity_lat = $this->view->getRequest()->get('proximity_lat', '');
$form['proximity_lat'] = [
'#type' => 'textfield',
'#title' => $this->t('Latitude'),
'#empty_value' => '',
- '#default_value' => $this->view->getRequest()->get('proximity_lat', ''),
+ '#default_value' => $proximity_lat,
'#maxlength' => 255,
'#weight' => -1,
];
+
+ $proximity_lng = $this->view->getRequest()->get('proximity_lng', '');
$form['proximity_lng'] = [
'#type' => 'textfield',
'#title' => $this->t('Longitude'),
'#empty_value' => '',
- '#default_value' => $this->view->getRequest()->get('proximity_lng', ''),
+ '#default_value' => $proximity_lng,
'#maxlength' => 255,
'#weight' => -1,
];
@@ -503,6 +528,7 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
if (
$this->options['proximity_geocoder']
&& !empty($this->options['proximity_geocoder_plugin_settings'])
+ && $proximity_source !== 'client_location'
) {
$geocoder_configuration = $this->options['proximity_geocoder_plugin_settings']['settings'];
$geocoder_configuration['label'] = $this->t('Address');
@@ -533,6 +559,64 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
$form['actions']['submit']['#value'] = $this->t('Calculate proximity');
+ // Add functionality for retrieving location from client
+ // using HTML5 geolocation API.
+ if ($proximity_source === 'client_location') {
+ // Determine if we already have coordinate data, either retrieved from the
+ // current request or stored in a cookie.
+ $has_coordinates = ((!empty($proximity_lat) && !empty($proximity_lng)) ||
+ $this->getStoredUserGeolocation());
+
+ // If AJAX is not enabled on this view, then enable it and run
+ // Views pre-render code to generate the AJAX settings for the
+ // view. These AJAX settings are used by the HTML5 geolocation
+ // code to auto-update the view after the HTML5 client location
+ // is retrieved.
+ if (!$this->view->ajaxEnabled() && !$has_coordinates) {
+ $this->view->setAjaxEnabled(TRUE);
+ views_views_pre_render($this->view);
+ }
+
+ // By default, the view will auto-refresh using AJAX once HTML5
+ // coordinates are received.
+ $auto_refresh = true;
+
+ // Determine if this view requires exposed form input and if any
+ // exposed input exists. If the exposed form requires input and
+ // none exists, then the view will not be auto-refreshed via
+ // AJAX.
+ $exposed_form_plugin = $this->view->display_handler->getPlugin('exposed_form');
+ if ($exposed_form_plugin instanceof InputRequired) {
+ $auto_refresh = (!empty($this->view->getExposedInput()));
+ }
+
+ // Add assets and drupalSettings for HTML5 geolocation.
+ $form = array_merge_recursive($form, [
+ '#attached' => [
+ 'library' => [
+ 'geolocation/geolocation.proximity.html5',
+ ],
+ 'drupalSettings' => [
+ 'geolocation' => [
+ 'html5' => [
+ 'hasCoordinates' => $has_coordinates,
+ 'proximity_view_ids' => [$this->view->dom_id],
+ $this->view->dom_id => [
+ 'auto_refresh' => $auto_refresh,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ // Remove the form inputs, as the view will be dynamically
+ // refreshed if HTML5 geolocation succeeds.
+ unset($form['proximity_lat']);
+ unset($form['proximity_lng']);
+ unset($form['actions']);
+ }
+
// #weight will be stripped from 'output' in preRender callback.
// Offset negatively to compensate.
foreach (Element::children($form) as $key) {
@@ -561,4 +645,34 @@ class ProximityField extends NumericField implements ContainerFactoryPluginInter
return parent::render($row);
}
+ /**
+ * Store the client location in temporary storage.
+ *
+ * @param $latitude
+ * The latitude coordinate.
+ * @param $longitude
+ * The longitude coordinate.
+ */
+ protected function storeUserGeolocation($latitude, $longitude) {
+ if (empty($latitude) || empty($longitude) ||
+ $this->view->getRequest()->cookies->has('client_location')) {
+ return;
+ }
+ setCookie('client_location', "{$latitude}:{$longitude}", 0, '/', $this->view->getRequest()->getHost(), FALSE, TRUE);
+ }
+
+ /**
+ * Retrieve the client location from temporary storage.
+ *
+ * @return bool|mixed
+ * Return FALSE if no client location cookie exists, else return the cookie
+ * value.
+ */
+ protected function getStoredUserGeolocation() {
+ if (!$this->view->getRequest()->cookies->has('client_location')) {
+ return FALSE;
+ }
+ return $this->view->getRequest()->cookies->get('client_location');
+ }
+
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment