social_geolocation-8.x-1.2/social_geolocation.module
social_geolocation.module
<?php
/**
* @file
* Contains hook implementations for the Social Geolocation module.
*
* @todo Add update hook to convert settings to new keys.
*/
use Drupal\address\AddressInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Link;
use Drupal\Core\Utility\Error;
use Drupal\group\Entity\GroupInterface;
use Drupal\node\NodeInterface;
use Drupal\profile\Entity\ProfileInterface;
use Drupal\user\Entity\User;
/**
* Implements hook_help().
*/
function social_geolocation_help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.social_geolocation':
$text = file_get_contents(__DIR__ . '/README.md');
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . Html::escape($text) . '</pre>';
}
else {
// Use the Markdown filter to render the README.
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::configFactory()->get('markdown.settings')->getRawData();
$config = ['settings' => $settings];
$filter = $filter_manager->createInstance('markdown', $config);
return $filter->process($text, 'en');
}
}
return NULL;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Enhance the Views exposed filter blocks forms on the people overview.
*/
function social_geolocation_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
$filter_forms = [
'views-exposed-form-user-admin-people-page-1',
];
if (!in_array($form['#id'], $filter_forms, TRUE)) {
return;
}
// @todo Try to create custom filter instead of using form_alter.
social_geolocation_attach_views_location_filter($form, 'fieldset');
// Clean up profile geolocation filter fields added by default.
unset($form['center']);
$geolocation_field_container = &$form['filters']['children']
['container-root']['children']
['container-container-1']['children']
['container-container-8']['children'];
// Hide fields that shouldn't be shown.
hide($form['field_profile_geolocation_proximity_center']);
hide($geolocation_field_container['field_profile_geolocation_proximity']);
}
/**
* Alters the form to attach the proximity filter.
*
* @param array $form
* The form to alter.
* @param string $container_type
* What Form API element should be used for the container of the proximity
* filter, either 'details' or 'fieldset'.
*/
function social_geolocation_attach_views_location_filter(array &$form, string $container_type = 'details'): void {
$geolocation_field_container = &$form['filters']['children']
['container-root']['children']
['container-container-1']['children']
['container-container-8']['children'];
$geocoder_plugin = _social_geolocation_get_geocoder();
if (empty($geocoder_plugin)) {
return;
}
$range_min = 10;
$range_max = 1000;
$unit_of_measurement = \Drupal::config('social_geolocation.settings')->get('unit_of_measurement');
$form['#validate'][] = '_social_geolocation_form_views_exposed_form_validate';
// Set up a container for our location filtering fields.
$geolocation_field_container['location_details'] = [
'#title' => t('Location'),
'#type' => $container_type,
'#weight' => 50,
];
$geolocation_field_container['location_details']['proximity'] = [
'#type' => 'number',
'#title' => t('Distance'),
'#description' => t("From {$range_min} to {$range_max} {$unit_of_measurement}"),
'#min' => $range_min,
'#max' => $range_max,
'#weight' => 10,
];
$identifier = 'proximity';
// Add the geocoder field in our container.
$geocoder_plugin->formAttachGeocoder($geolocation_field_container['location_details'], $identifier);
// Change the title to how it should be displayed in Open Social.
$geolocation_field_container['location_details']['geolocation_geocoder_address']['#title'] = t('Address');
$geolocation_field_container['location_details']['geolocation_geocoder_address']['#placeholder'] = t('City, Country');
$geolocation_field_container['location_details']['geolocation_geocoder_address']['#size'] = 30;
unset($geolocation_field_container['location_details']['geolocation_geocoder_address']['#description']);
// Add the required libraries and javascript settings.
$form['#attached']['library'][] = 'core/drupal.ajax';
$form['#attached']['library'][] = 'geolocation/geolocation.views.filter.geocoder';
$form['#attached']['library'][] = 'social_geolocation/social_geolocation.location';
$form['#attached']['drupalSettings']['geolocation']['geocoder'] = [
'viewsFilterGeocoder' => [
$identifier => [
'type' => 'proximity',
],
],
];
}
/**
* Validate function for Geolocation exposed form filter.
*/
function _social_geolocation_form_views_exposed_form_validate(&$form, FormStateInterface $form_state): void {
$geolocation_field_container = $form['filters']['children']
['container-root']['children']
['container-container-1']['children']
['container-container-8']['children']
['location_details'] ?? $form['location_details'];
// Address is a string filled into the exposed views filter.
$address = $form_state->getValue('geolocation_geocoder_address');
if (empty($address)) {
return;
}
$address_geocoded = _social_geolocation_geocode_address($address);
if (!empty($address_geocoded)) {
// Set values for field_profile_geolocation_proximity. Default is 20.
$proximity = $form_state->getValue('proximity');
if (!empty($form_state->getValue('proximity'))) {
$form_state->setValue('field_profile_geolocation_proximity', $proximity);
}
$form_state->setValue([
'field_profile_geolocation_proximity_center',
'coordinates',
'lat',
], $address_geocoded['lat']);
$form_state->setValue([
'field_profile_geolocation_proximity_center',
'coordinates',
'lng',
],
$address_geocoded['lng']);
}
else {
$element = ['geolocation_geocoder_address'];
if ($geolocation_field_container !== NULL) {
$element = $geolocation_field_container['geolocation_geocoder_address'];
}
$form_state->setError($element, t('Sorry, we tried to find your address, but couldn’t find it. Double-check your spelling and try again. If you still get an error, please try again later.'));
}
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function social_geolocation_group_presave(GroupInterface $group): void {
_social_geolocation_entity_presave($group, 'group');
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function social_geolocation_node_presave(NodeInterface $node): void {
if ($node->getType() == 'event') {
_social_geolocation_entity_presave($node, 'event');
}
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function social_geolocation_profile_presave(ProfileInterface $profile): void {
_social_geolocation_entity_presave($profile, 'profile');
}
/**
* Set value to geolocation field based on address input.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity that is being saved.
* @param string $type
* The type of the entity being saved.
*/
function _social_geolocation_entity_presave(FieldableEntityInterface $entity, string $type): void {
$enabled = \Drupal::config('social_geolocation.settings')->get('enabled');
if (!$enabled) {
return;
}
$field_address = "field_{$type}_address";
$field_geolocation = "field_{$type}_geolocation";
// We require both an address field to geocode and a geolocation field to
// store the result in.
if (!$entity->hasField($field_address) || !$entity->hasField($field_geolocation)) {
return;
}
$empty_address = $entity->get($field_address)->isEmpty();
$empty_geolocation = $entity->get($field_geolocation)->isEmpty();
$is_updating = !empty($entity->original);
// If the entity has no address and no stored geolocation then there's also
// nothing to do. Otherwise the geolocation field needs to be updated.
if ($empty_address && $empty_geolocation) {
return;
}
// If the address hasn't been changed and there's already a geolocation stored
// then the geolocation doesn't need to be updated.
if (!$empty_geolocation && $is_updating &&
$entity->original->get($field_address)->getValue() === $entity->get($field_address)->getValue()) {
return;
}
// If we had a geolocation value but no longer have an address then we clear
// the geolocation value.
if ($empty_address && !$empty_geolocation) {
$entity->set($field_geolocation, NULL);
return;
}
$new_coordinates = NULL;
// Format address as a string consumable by the geocoding API.
/** @var \Drupal\address\AddressInterface $address */
$address = $entity->get($field_address)->first();
$address = _social_geolocation_address_to_string($address);
// Convert formatted string to a set of coordinates for the geolocation field.
$location = _social_geolocation_geocode_address($address);
// Check whether we should show a help message on failed geolocation.
$site_manager_assist = \Drupal::config('social_geolocation.settings')->get('site_manager_assistance');
if (!empty($location)) {
$new_coordinates = [
'lat' => $location['lat'],
'lng' => $location['lng'],
'lat_sin' => sin(deg2rad($location['lat'])),
'lat_cos' => cos(deg2rad($location['lat'])),
'lng_rad' => deg2rad($location['lng']),
];
}
elseif ($site_manager_assist) {
$contact = 'site manager';
// If the private message module is enabled then we create a link to contact
// the site manager by private message.
if (\Drupal::moduleHandler()->moduleExists('social_private_message')) {
$site_manager = \Drupal::config('social_geolocation.settings')->get('site_manager_contact');
// We can only link to a site manager if we have one configured and
// it's a valid user id.
if ($site_manager !== NULL && User::load($site_manager) !== NULL) {
$contact = Link::createFromRoute($contact, 'private_message.private_message_create', [], [
'query' => ['recipient' => $site_manager],
])->toString();
}
}
\Drupal::messenger()->addWarning(
t("Unfortunately we can't locate the address you entered. Please update it or contact a @contact if you would like the event to show up in a search by location.", [
'@contact' => $contact,
])
);
}
$entity->set($field_geolocation, $new_coordinates);
}
/**
* Retrieves the configured Geocoder plugin.
*
* @return \Drupal\geolocation\GeocoderInterface|false
* Returns the configured geocoder or false if none is configured or the
* configured geocoder could not be loaded.
*/
function _social_geolocation_get_geocoder() {
$geocoder_plugin = &drupal_static(__FUNCTION__);
if (!isset($geocoder_plugin)) {
$geocoder_plugin_id = \Drupal::config('social_geolocation.settings')
->get('geolocation_provider');
$geocoder_plugin = \Drupal::service('plugin.manager.geolocation.geocoder')
->getGeocoder(
$geocoder_plugin_id,
// The configuration object passed to the plugins is not actually used
// by the geocoders implemented in the Geolocation module. Configuration
// happens through configuration objects that are loaded directly from
// the config factory (e.g. see
// Drupal\geolocation_google_maps\Plugin\geolocation\Geocoder\GoogleGeocodingAPI::geocode()).
[]
);
}
return $geocoder_plugin;
}
/**
* Convert address to geolocation values.
*
* @param string $address
* The address that can be given to the Geocoder::geocode method.
*
* @return array
* An array with a status field and lat/lng values if a geolocation was found.
*/
function _social_geolocation_geocode_address(string $address): array {
// If there's no address to geocode or we have no geocoder service
// then there's nothing to do.
if (empty($address)) {
return [];
}
$geocoder = _social_geolocation_get_geocoder();
if (empty($geocoder)) {
return [];
}
try {
$result = $geocoder->geocode($address);
} catch (Exception $e) {
// Save exception to be logged.
$logger = \Drupal::logger('social_geolocation');
Error::logException($logger, $e);
// Add warning message to notice the user about location.
$warning_message = \Drupal::translation()->translate('Unable to get location, address fields kept empty.');
\Drupal::messenger()->addWarning($warning_message);
// Return early with no data.
return [];
}
if (empty($result)) {
return [];
}
return $result['location'];
}
/**
* Converts an address field value to a string for a geocoding API.
*
* Uses the formatter provided by the CommerceGuys/Addressing library which
* takes into account the locale of the selected address. This should result in
* a proper lef-to-right string that can be consumed by at least Nominatim and
* the Google Geocoding API.
*
* @param \Drupal\address\AddressInterface $address
* The address field.
*
* @return string
* The string that can be sent to a geocoding API.
*/
function _social_geolocation_address_to_string(AddressInterface $address) : string {
// Fix any edge cases in address formatting for the API we're using.
$address_changes = _social_geolocation_reformat_address_for_api($address);
// Format the address as a plain-text block.
$formatted_address = implode(', ', $address->toArray());
// Revert any changes that were made to comply with what the API expects so
// the address values are stored in the way the user entered them.
foreach ($address_changes as $field => $value) {
$address->set($field, $value);
}
return $formatted_address;
}
/**
* Fixes edge cases in address values before they're formatted.
*
* @param \Drupal\address\AddressInterface $address
* The address field.
*
* @return array
* An associative array with keys for the fields that were changed and values
* of the original values for that field. Can be used to restore the Address
* to its original state for storage in the database. Returns an empty array
* if no changes were made.
*/
function _social_geolocation_reformat_address_for_api(AddressInterface $address) : array {
$geocoder = _social_geolocation_get_geocoder();
if ($geocoder !== FALSE && $geocoder->getPluginId() === 'nominatim') {
switch ($address->getCountryCode()) {
// Nominatim doesn't handle postal codes for Canada well.
// See Issue #3086891.
// To resolve this we remove the postal code before lookup.
case 'CA':
$postal_code = $address->get('postal_code')->getValue();
$address->set('postal_code', '');
return ['postal_code' => $postal_code];
// Nominatim doesn't handle spaces in the postalcode for the Netherlands.
// To resolve this any spaces are removed.
case 'NL':
$postal_code = $address->get('postal_code')->getValue();
$address->set('postal_code', preg_replace('/\s/', '', $postal_code));
return ['postal_code' => $postal_code];
}
}
return [];
}
