First of all, thanks for this great module. It's been a lifesaver.

One feature that I am interested in is using HTML5 browser geolocation as the source for latitude/longitude to calculate proximity for map results and sort them. I can get fairly close to satisfying this use case by using an exposed Proximity Filter as the input source for the Proximity Field, but it's not quite what I'm looking for. I've seen other issues that seem related, like #2581031: Make HTML browser location and geocoding widget available as exposed filters and #2841730: Proximity settings source type - static values only?, but they don't quite seem to be the same thing.

If I use the exposed proximity filter without geocoding options, I get fields for latitude/longitude, but the "distance" field seems to be required. Of course, this makes sense for a proximity filter designed to filter results by how close they are to a location, but in my case, I only care about determining the proximity distance and not filtering results based on it.

I've also tried configuring the view to use "user input" as the source for calculating the proximity field distance. This is closer to what I want in that it just provides fields for latitude/longitude, but it seems to require using query parameters to provide values for these fields to the view.

So it seems to me that the most likely solution here would be adding a new source option to the Proximity Field (since I don't need filtering). The source would be "HTML5 Geolocation" and I think it could work something like this:

  1. Latitude/longitude fields are hidden. Results are hidden by default.
  2. Browser requests HTML5 location.
    1. If successful, coordinates are pushed to latitude/longitude fields and View is reloaded via AJAX, revealing the results with proximity distance.
    2. If unsuccessful, default View results are shown (proximity distance will be empty).

I notice you outlined somewhat similar functionality in #2822539-13: Order proximity source options. I'm not sure quite how the coordinate data could be received by the view, even via AJAX, since it's not exposed data, but I can look into it.

I'm happy to contribute to make this feature a reality, but any guidance you want to provide on implementation is welcome. Thanks!

CommentFileSizeAuthor
#38 2879171-38-location-input.patch5.19 KBChristianAdamski
#36 2879171-36-geolocation-html5-proximity.patch21.58 KBalbertski
#34 2879171-33-geolocation-html5-proximity.patch41.92 KBalbertski
#28 2879171-28-geolocation-html5-proximity.patch42.43 KBwelly
#23 interdiff-2879171-23-21.txt2.58 KBrecrit
#23 2879171-23-geolocation-html5-proximity.patch21.67 KBrecrit
#21 2879171-21-geolocation-html5-proximity.patch20.75 KBmrkdboyd
#20 2879171-20-geolocation-html5-proximity.patch20.56 KBmrkdboyd
#18 2879171-18-geolocation-html5-proximity.patch20.36 KBmrkdboyd
#16 2879171-15-html5-geolocation-proximity.patch17.23 KBmrkdboyd
#12 2879171-12-html5-geolocation-proximity.patch15.25 KBmrkdboyd
#11 2879171-11-html5-geolocation-proximity.patch14.02 KBmrkdboyd
#6 2879171-6-html5-geolocation-proximity.patch11.91 KBmrkdboyd
#5 2879171-5-html5-geolocation-proximity.patch7.64 KBmrkdboyd
#4 2879171-4-html5-geolocation-proximity.patch9.22 KBmrkdboyd
#3 2879171-3-html5-geolocation-proximity.patch5.54 KBmrkdboyd
#2 2879171-2-html5-geolocation-proximity.patch3.15 KBmrkdboyd
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mrkdboyd created an issue. See original summary.

mrkdboyd’s picture

After testing a few different approaches, I'm attaching a very rough first pass at this. I have added a "Client location" option for the proximity field source, and if this option is chosen, then a form for entering the latitude/longitude appears, the same as if "User input" is chosen.

However, rather than submitting the form via GET for regular "User input", for "Client location", the form submits via POST. Ultimately, the idea is that these form elements would be hidden rather than visible, receive their values dynamically if HTML5 browser geolocation succeeds, and then have the form be submitted via AJAX to update the view.

mrkdboyd’s picture

Update: I was having all sorts of trouble getting the View to successfully refresh via AJAX. So instead, I pivoted to keeping the simple behavior of having proximity coordinates come in via query parameters.

I'm dynamically populating the coordinates from HTML5 geolocation if it is successful and auto-submitting the coordinate input form to refresh the page with proximity distances calculated. And I added a simple drupalSetting to avoid repeating this process if there are already coordinates in the request to avoid an endless page refresh loop.

This approach works, but it's fairly undesirable to refresh someone's page on every visit to a new view with proximity fields. Perhaps one enhancement would be to store the geolocation data somehow so that it could be directly used on subsequent pages without an HTML5 geolocation request, but I think there are some security/privacy concerns there.

mrkdboyd’s picture

I finally got my head around what I needed to do here and got this working. I also introduce a new library/Drupal behavior for just handling integration with the HTML5 Geolocation API. It would fairly easy to update other uses of the API (common map, HTML5 widget) to use this behavior, but of course only if the changes seems worthwhile to you. Let me know what you think.

Now that the basic functionality is working, the next enhancement I have in mind would be to store the geolocation coordinates in a secure cookie for the current domain only, "HttpOnly = true" so that it cannot be read from Javascript, and only persisting for the length of the session. This would allow the query for proximity distance on subsequent requests to just retrieve the stored geolocation, rather than having to retrieve it again from the client.

mrkdboyd’s picture

Added functionality to store the client location in a cookie. Also added some code to ensure AJAX processing works even if the view isn't set to use AJAX.

mrkdboyd’s picture

Sigh...last patch was missing the two JS files I added. Here is an updated patch with a few minor code style fixes.

Status: Needs review » Needs work

The last submitted patch, 6: 2879171-6-html5-geolocation-proximity.patch, failed testing.

mrkdboyd’s picture

Status: Needs work » Needs review
attishu’s picture

Interested...

mrkdboyd’s picture

Just realized that it would be better to use the TempStore provided by Drupal core for users to store geolocation data in the session instead of a cookie. I'll work on that and re-roll the patch.

mrkdboyd’s picture

There is a bug in Drupal core where the TempStore isn't properly initialized for anonymous users. See #2743931: Saving to the private tempstore doesn't start a session for anonymous users. So I'm putting that re-factor on hold for the moment.

However, I did refactor the JS code for HTML5 geolocation to more closely mimic the behavior of the JS for the geocoder, where arbitrary JS code can "subscribe" to the HTML5 geolocation result by registering a callback. I also updated the code to namespace the drupalSettings to the module.

mrkdboyd’s picture

Re-rolled patch with a small fix to ensure that views aren't auto-refreshed if they have required exposed filters and there is no input for exposed filters

Ruslan Piskarov’s picture

Hello Mark,

I am testing your patch and trying to implement the following functionality:
I already have the map like this http://take.ms/2HXCR (important things marked as red).
So, then I am typing address, e.g. London and put Destination, I see the centre of London plus 15 miles.
However, by default, without "address" & "destination" I want to see venues near me in radius 15 miles.
Could you explain more about you patch?
Because I found the settings of the 'Client location (HTML5)' there http://take.ms/f3GBd (http://take.ms/jF1Fr) and can not understand how to tie it with "Filter criteria". I believe it should be like this http://take.ms/xBiE8, but my opinion can be wrong. In the result, I got this map http://take.ms/haD9W.

Could you review my comment?
Thank you in advance.

mrkdboyd’s picture

@Ruslan P - So this patch does not integrate with filter criteria. It is only for use when you want to calculate proximity distance, but not to use that distance as filter criteria (which is what you are trying to do). For what you are trying to do, I think you need to change the option here (http://take.ms/jF1Fr) to be "Proximity Filter" instead of "Client location (HTML5)" and then configured it to use your proximity filter on the view.

The "Client location (HTML5)" option that I added for proximity fields is basically an enhancement to the "User input" option. Whereas the "User input" option gives non-filter fields for latitude/longtitude for calculating proximity distance to geolocated entities, this patch provides integration with HTML5 geolocation so that a user's location is automatically determined (if they give permission) and then the view results are refreshed via AJAX with the calculated proximity distances. Also, once the user's geolocation has been determined, it is stored in a secure session cookie so that subsequent visits to pages with geolocated entities can pull the user geolocation from the cookie, rather than requiring another round-trip from the front-end using AJAX.

Ruslan Piskarov’s picture

@mrkdboyd, thank you. Will try again after your comment.

mrkdboyd’s picture

Re-rolled patch with a couple updates:

  • Updated JS to follow module conventions more closely
  • Added use of $.Deferred() to delay the AJAX map update until it is fully loaded and coordinates are received
  • Added a call to clearMarkers() for the marker clusterer. Without this change, I was seeing duplicate marker clusterers when the view was refreshed via AJAX, even though Drupal.geolocation.removeMapMarkers() was successfully running during the AJAX update.
whitelancer’s picture

@mrkdboyd,

I have been trying to find a way to replicate a function I've used on older Drupal sites -- simply sorting a set of results based on closest to the user, using the HTML5 location / api built into browsers. I am pretty surprised that there just doesn't seem to be anything like this out there, so I'm attempting to use your patch as a starting place to get this going.

I've patched your code, but so far, I've not been able to see this working. This doesn't seem to be prompting the user for their location, and so I'm never getting any geolocation results back (for the user). Was there anything special that needs to be done in order to prompt the user's browser to share the location? In the past I've never had any issues getting this to work, but I'm still working though the changes to Drupal 8.

Also, what would be involved to extend your patch so it can support sorting closest to the user?

Thanks for your contribution!!

[EDIT]

I discovered the issue here. I have been using one list as results, and a map attachment floated to the right, to display the map. I had to remove references to a map from the code, as the promise was never resolving for mapLoaded.

The only other question at this point would be -- do you have any ideas on how I might display nothing if location is denied or cannot be determined? I'm basically building a directory listing sorted by closest to the user, but it is pointless if the location cannot be determined. I wanted to use a filter when the proximity was 0, but since this code is focused on the field, I wasn't sure the best way to handle that. I'm looking into a few options to refresh and change settings and the like, but I was hoping maybe you had a solution you might recommend.

Thank you!

mrkdboyd’s picture

Re-rolled patch with a couple updates:

  • Fixed issue with mapLoaded promise blocking geolocation for views with no maps.
  • Abstracted storage of geolocation within session to GeolocationTempStore service. Would ultimately like to integrate with the Drupal user private temporary storage once #2743931: Saving to the private tempstore doesn't start a session for anonymous users lands.
  • Allow stored geolocation if geolocation coordinate request parameters are different.
mrkdboyd’s picture

@whitelancer - Sorry for the delay, but the above patch should address the issue you were seeing with the mapLoaded promise.

If you want to actually hide the view based on geolocation error, it should be possible to add some code to register custom callbacks in case of HTML5 geolocation failure, the same way that success callbacks are registered in the current patch. Then in your callbacks you could take whatever action you like. I think you would have to go the JS route, since HTML5 geolocation happens on the client.

mrkdboyd’s picture

Minor update to previous patch to consolidate some logic into the new GeolocationTempStore service.

mrkdboyd’s picture

Fixing an issue in the previous patch with false positives for stored geolocation in the session which caused geolocation Views to never auto-refresh after receiving coordinates from the browser.

jlballes’s picture

The patch works well!
Is it possible do not show results until the ajax request?
Is it going to be merged to the module code?
Thanks!

recrit’s picture

Added an error callbacks api so that custom JS can respond to errors such as the user denying the location.

galvestor’s picture

I get "bad git-diff - expected dev/null on line 23" when trying to patch it with git.

Am I missing something here?

galvestor’s picture

ok, resolved part of it. However, I still get the following when trying to apply the last patch:

error: patch failed: js/geolocation-google-maps-api.js:434
error: js/geolocation-google-maps-api.js: patch does not apply

Ruslan Piskarov’s picture

10 Sep 2017 was new DEV version of the module. Probably we need to update the patch to.

rgeerolf’s picture

I rolled back to commit #cb68b54 so the patch would apply. I set the client source to 'Client location (HTML5)' but when I check the view I do not get prompted from my browser (tried multiple) for my location. Is there something I'm missing here? I'm just trying to built a list of locations, there is not map involved.

#17 seemed to have a simular problem but it does not look the same issue.

welly’s picture

I've rerolled the patch to work on the latest version but I can't actually get it working. It never seems to prompt the user for their location. I'll have another look and see if I can see what's occurring (or not in this case) as I definitely need this for a current project.

rgeerolf’s picture

@welly

It never seems to prompt the user for their location.

The problem for me was that I was in my dev environment and I was not using https. In Firefox 55 or Chrome 50 and higher, only HTTPS pages will be able to request location. Switching to https fixed the issue for me.

Status: Needs review » Needs work

The last submitted patch, 28: 2879171-28-geolocation-html5-proximity.patch, failed testing. View results

welly’s picture

Thanks @rgeerolf, I'll try that out and see if it makes any difference!

SocialNicheGuru’s picture

I think it needs to be rerolled again for the latest 1.x version

joannamay’s picture

Any updates on this? I'm able to make the changes manually by combing through the patch, but composer chokes on running the patch.

albertski’s picture

Component: HTML5 geolocation » Geolocation Field / Backend
FileSize
41.92 KB

I have rerolled the patch but also haven't gotten it to work.

albertski’s picture

I updated the beginning of viewsForm() to this and now I am seeing that it asks for Geo Locations but it still does not work.

if ($this->options['proximity_source'] != 'user_input' && $this->options['proximity_source'] != 'client_location') {
      unset($form['actions']);
      return;
    }

@mrkdboyd (or anyone that understands this) I am going through geolcations-html.js and I am not understanding how data gets added to Drupal.geolocation.html5.resultCallbacks (Drupal.geolocation.html5.resultCallbacks is empty so it does not go any further). I don't see anything calling addResultCallback().

albertski’s picture

The attached patch is #23 rerolled for the latest. I was seeing better results compared to the one from #34. I still wasn't able to quite get the results I wanted so I created a custom field formatter (overwrote ProximityField and extended it) and used 1.1.

In my view:
- Add proximity field as a field. Choose User input
- Make sure to set view to use Ajax

my_module.views.inc:


/**
 * @file
 * Provide views data for my module.
 */

/**
 * Implements hook_views_plugins_field_alter().
 */
function mymodule_views_plugins_field_alter(array &$plugins) {
  $plugins['geolocation_field_proximity']['class'] = 'Drupal\mymodule\Plugin\views\field\RangeProximityField';
}

RangeProximityField.php

namespace Drupal\mymodule\Plugin\views\field;

use Drupal\geolocation\Plugin\views\field\ProximityField;
use Drupal\Core\Form\FormStateInterface;

/**
 * Field handler for geolocation field.
 *
 * The Proximity Field handler (at the time) did not have a working ability
 * for handling range based on user's location.  This overwrites the
 * ProximityField handler and makes changes as needed.
 *
 * @ingroup views_field_handlers
 *
 * @ViewsField("geolocation_field_proximity")
 */
class RangeProximityField extends ProximityField {

  /**
   * {@inheritdoc}
   */
  public function query() {
    parent::query();

    // If range is set only display items within range.
    if ($range = $this->view->getRequest()->get('range', FALSE)) {
      $this->query->addHavingExpression(0, $this->field_alias . ' <= :num', [':num' => $range]);
    }
  }

  /**
   * Form constructor for the user input form.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function viewsForm(array &$form, FormStateInterface $form_state) {
    parent::viewsForm($form, $form_state);

    $range = $this->view->getRequest()->get('range', FALSE);
    $form['range'] = [
      '#type' => 'select',
      '#options' => [
        0 => 'All Dealers',
        40 => '40 miles away',
        60 => '60 miles away',
        80 => '80 miles away',
        100 => '100 miles away',
        200 => '200 miles away',
      ],
      '#default_value' => $range,
      '#empty_option' => t('Choose Distance From Your Current Location'),
      '#maxlength' => 255,
      '#weight' => -100,
      '#required' => TRUE,
    ];

    // Sets the fields to hidden.
    $form['proximity_lat']['#type'] = 'hidden';
    $form['proximity_lng']['#type'] = 'hidden';

    // Hides the submit button.
    unset($form['actions']);

    // Add Library for handling proximity.
    $form['#attached']['library'] = [
      'mymodule/range_proximity',
    ];
  }

}

mymodule_range_proximity.js

/**
 * @file
 *   Javascript for integrating the W3C Geolocation API.
 */

(function ($, Drupal, drupalSettings, navigator) {

  'use strict';

  /**
   * @namespace
   */
  Drupal.range_proximity = Drupal.range_proximity || {};
  Drupal.range_proximity.html5 = Drupal.range_proximity.html5 || {};

  /**
   * Attach HTML5 geolocation functionality.
   *
   * @type {Drupal~behavior}
   */
  Drupal.behaviors.mymodule_geolocationHTML5 = {
    attach: function (context) {
      $.each(drupalSettings.views.ajaxViews, function (index, view) {
        $('.js-view-dom-id-' + view.view_dom_id + ' select').on('change', function () {
          Drupal.range_proximity.html5.getClientLocation(this.value, context);
        });
      });
    }
  };

  /**
   * Refresh view.
   *
   * @param string domid
   *   Element dom id.
   * @param object coordinates
   *   Coordinates.
   * @param int range
   *   Range.
   * @param object context
   *   Context object.
   */
  Drupal.range_proximity.html5.refreshView = function(domid, coordinates, range, context) {
    // Get the AJAX settings for this view.
    var viewSettings = Drupal.views.instances['views_dom_id:' + domid];
    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;
    geolocationAjaxSettings.submit['range'] = range;

    // Use AJAX to refresh the view.
    $('.js-view-dom-id-' + domid).trigger('RefreshView');
  };

  /**
   * Get client location.
   *
   * @param int range
   *   Range.
   * @param object context
   *   Context object.
   */
  Drupal.range_proximity.html5.getClientLocation = function (range, context) {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(function (position) {
        $.each(drupalSettings.views.ajaxViews, function (index, view) {
          Drupal.range_proximity.html5.refreshView(view.view_dom_id, position.coords, range, context)
        });
      }, function (error) {
        if (error.code === error.PERMISSION_DENIED) {
          var message = '<div class="geo-permission-denied alert alert-info" role="alert">Your browser\'s gelocation was declined for this website. Please try again or check your browser settings.</div>';
          Drupal.range_proximity.html5.removePermissionDeniedMessage();
          $('h1').after(message);
        }
      });
    }
  };

  /**
   * Removes the permission denied message.
   */
  Drupal.range_proximity.html5.removePermissionDeniedMessage = function () {
    $('.geo-permission-denied').remove();
  };

})(jQuery, Drupal, drupalSettings, navigator);

ChristianAdamski’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev

In #2936637: In "Filter Criteria" the "Proximity" filter does not work (v2) the whole proximity handling was massively reworked. The "client location" behavior as described here is now fairly easy to built. I couldn't though, because my local dev setup does not support client location. Will do so in a bit.

ChristianAdamski’s picture

Status: Needs work » Needs review
FileSize
5.19 KB

Straight forward location input plugin.
- set client location if available
- optionally hide form
- optionally auto-submit

P.S. The range thingie should go to its issue. It does not really belong here, but would be a nice feature.

  • ChristianAdamski committed e8c117d on 8.x-2.x
    Issue #2879171 by mrkdboyd, albertski, recrit, ChristianAdamski, welly:...
ChristianAdamski’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.