ip_geoloc-2.0.0-alpha0/src/Services/IpGeoLocAPI.php
src/Services/IpGeoLocAPI.php
<?php
namespace Drupal\ip_geoloc\Services;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Render\Markup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Database\Connection;
use GuzzleHttp\Exception\RequestException;
use \PDOException;
define('IP_GEOLOC_GOOGLE_MAPS', ip_geoloc_build_google_api_url());
define('IP_GEOLOC_GOOGLE_MAPS_SERVER', 'https://maps.googleapis.com/maps/api/geocode/json');
/**
* Class IpGeoAPI to interact with other modules.
*/
class IpGeoLocAPI {
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $logger;
/**
* The IP geolocation session.
*
* @var \Drupal\ip_geoloc\Services\IpGeoLocSession
*/
protected $ipGeolocSession;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The configuration service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The file URL generator service.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
protected $routerAdminContext;
/**
* The current logged user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a new IpGeoLocGlobal object. Adds dependency injection.
*/
public function __construct(LoggerChannelFactoryInterface $logger, IpGeoLocSession $ipGeolocSession, TranslationInterface $stringTranslation, MessengerInterface $messenger,ConfigFactoryInterface $config_factory, FileUrlGeneratorInterface $fileUrlGenerator, $routerAdminContext, AccountProxyInterface $currentUser, Connection $database) {
$this->logger = $logger;
$this->ipGeolocSession = $ipGeolocSession;
$this->stringTranslation = $stringTranslation;
$this->messenger = $messenger;
$this->config = $config_factory->get('ip_geoloc.settings');
$this->fileUrlGenerator = $fileUrlGenerator;
$this->routerAdminContext = $routerAdminContext;
$this->currentUser = $currentUser;
$this->database = $database;
}
/**
* Store the supplied IP geolocation info on the database. This will overwrite any existing info for the IP address in question.
*
* @param array $location
* Array with up to 13 location info fields; must at least
* contain a non-empty $location['ip_address'] and a non-empty
* $location['formatted_address'] for anything to happen.
*
* @return mixed
* Returns 0, when no insert or update was necessary
* SAVED_NEW (=1), when a new location record was inserted into the db
* SAVED_UPDATED (=2), when an existing location record was updated
* FALSE, when a db insert or db update failed
*/
public function storeLocation(array $location) {
// Give contributed modules a chance to add their 2 cents by implementing
// hook_get_ip_geolocation_alter()
// @TODO migrate drupal_alter
// drupal_alter('get_ip_geolocation', $location);.
$config = \Drupal::config('ip_geloc.settings');
$stores_addresses = $this->config->get('ip_geoloc_store_addresses') ? $this->config->get('ip_geoloc_store_addresses') : TRUE;
if (!$stores_addresses) {
return;
}
if (empty($location['ip_address']) || empty($location['formatted_address'])) {
// ip_geoloc_debug('IPGV&M: location object must contain both IP address
// and formatted address -- not stored.');.
return 0;
}
if ($location['ip_address'] != '127.0.0.1' && (!isset($location['latitude']) || !isset($location['latitude']))) {
$this->logger->get('IPGV&M')->warning('latitude or longitude missing for IP address %ip (location still stored)', ['%ip' => $location['ip_address']]);
}
// See if this IP is already on the db.
$connection = \Drupal::database();
$result = $connection->query('SELECT * FROM {ip_geoloc} WHERE ip_address = :ip', [':ip' => $location['ip_address']]);
$existing_location = $result->fetchAssoc();
if (!$existing_location) {
// New entry, insert.
$location['city'] = mb_convert_encoding($location['city'], "UTF-8", "ISO-8859-1");
$location['formatted_address'] = mb_convert_encoding($location['formatted_address'], "UTF-8", "ISO-8859-1");
// @TODO move debug to other location
// ip_geoloc_debug(t('IP Geolocaton: adding new record to db: !location', array('!location' => ip_geoloc_pretty_print($location))));
$full_location = &$location;
}
else {
// When updating, drupal_write_record() does not erase fields not present
// in $location.
$empty_location['latitude'] = '';
$empty_location['longitude'] = '';
$empty_location['country'] = '';
$empty_location['country_code'] = '';
$empty_location['region'] = '';
$empty_location['region_code'] = '';
$empty_location['city'] = '';
$empty_location['locality'] = '';
$empty_location['route'] = '';
$empty_location['street_number'] = '';
$empty_location['postal_code'] = '';
$empty_location['administrative_area_level_1'] = '';
$empty_location['formatted_address'] = '';
$location['id'] = $existing_location['id'];
$full_location = array_merge($empty_location, $location);
// @TODO move debug to other location
// ip_geoloc_debug(t('IPGV&M: updating db with above location'));
}
try {
$result = $connection->merge('ip_geoloc')
->fields($full_location);
if ($existing_location) {
$result->key(['id']);
}
$result->execute();
}
catch (PDOException $e) {
// May happen when a fields contains illegal characters.
$this->messenger->addMessage(Html::escape($e->getMessage()), 'error');
$result = FALSE;
}
if ($result === FALSE) {
$this->messenger->addMessage($this->stringTranslation->translate('IPGV&M: could not save location to db: !location', ['!location' => ip_geoloc_pretty_print($full_location)]), 'error');
}
return $result;
}
/**
* Outputs an HTML div placeholder into which will be injected a map.
*
* The map is centered on the supplied lat,long coordinates.
* Handy for use in Views.
*
* @param string|float $latitude
* E.g. "-37.87" or -37.87.
* @param string|float $longitude
* E.g. "144.98" or 144.98.
* @param string $div_id
* Id of the div placeholder, can be anything as long as it's unique.
* @param string $style
* CSS style applied to the div, e.g "height:250px; width:300px".
* @param string $balloon_text
* Text for hover or click.
*/
public function outputMap($latitude, $longitude, $div_id = 'ip-geoloc-map', $style = '', $balloon_text = '') {
//@TODO: Review how to migrate this.
// $script_code = "displayGMap($latitude, $longitude, '$div_id', '$balloon_text');";
// drupal_add_js($script_code, ['type' => 'inline', 'scope' => 'footer']);
$map_placeholder = "<div id='$div_id'" . (empty($style) ? '' : " style='$style'") . '></div>';
return $map_placeholder;
}
/**
* Outputs an HTML div placeholder into which will be injected a map.
*
* The locations to be mapped are supplied as an array of lat,long coordinates.
*
* @param array $locations
* Array of location objects each containing lat/long pair and optionally
* address, visit count and last visit to appear when the marker is clicked.
* @param string $div_id
* Id of the div placeholder, can be anything as long as it's unique.
* @param string $map_options
* As a JSON string, .e.g '{"mapTypeId":"roadmap", "zoom":15}'.
* @param string $map_style
* CSS style applied to the div, e.g "height:250px; width:300px".
*/
public function outputMapMultiVisitor( $locations, $div_id = 'ip-geoloc-map-multi-locations', $map_options = NULL, $map_style = NULL) {
// drupal_add_js(IP_GEOLOC_GOOGLE_MAPS, ['type' => 'external', 'group' => JS_LIBRARY]);
// drupal_add_js(\Drupal::service('extension.path.resolver')->getPath('module', 'ip_geoloc') . '/js/ip_geoloc_gmap_multi_visitor.js');
// if (!isset($map_options)) {
// $map_options = IP_GEOLOC_RECENT_VISITORS_MAP_OPTIONS;
// }
// $settings = [
// 'ip_geoloc_locations' => $locations,
// 'ip_geoloc_multi_location_map_div' => $div_id,
// 'ip_geoloc_multi_location_map_options' => drupal_json_decode($map_options),
// ];
// drupal_add_js($settings, 'setting');
if (!isset($map_style)) {
$map_style = IP_GEOLOC_MAP_DIV_DEFAULT_STYLE;
}
$map_placeholder = "<div id='$div_id'" . (empty($map_style) ? '' : " style='$map_style'") . '></div>';
return $map_placeholder;
}
/**
* Outputs an HTML div placeholder into which will be injected a Google map.
*
* The locations to be mapped are supplied as an array of location objects,
* each with lat,long coordinates and optional balloon text.
*
* Note this function will result in the visitor being prompted to share their
* location.
*
* @param array $locations
* Array of location objects each containing lat/long pair
* and optional balloon text.
* @param string $div_id
* Id of the div placeholder, can be anything as long as it's unique.
* @param string $map_options
* As a JSON string, .e.g '{"mapTypeId":"roadmap", "zoom":15}'.
* @param string $map_style
* CSS style applied to the div, e.g "height:250px; width:300px".
* @param string $marker_color
* Default color used for all locations that haven't had their
* color overridden, defaults to Google default (i.e. red marker with dot).
* @param bool|string $visitor_marker
* FALSE (no marker), TRUE (standard red marker) or a 'RRGGBB' color code.
* @param int $center_option
* Value 0: fixed cenrer, must be provided thorugh $map_options)
* Value 1: auto-center the map on the first location in the $locations array
* Value 2: auto-center the map on the visitor's current location.
* @param array $center_latlng
* Array of length 2 with lat/long coords used as a backup when
* $visitor_marker is set or $center_option == 2 and location could not be
* determined or $visitor_location_gps == FALSE.
* @param bool $visitor_location_gps
* Whether the HTML5-style location provider is to be
* used, if FALSE $center_latlng is used.
*
* @return string
* Map placeholder div.
*/
public function outputMapMultiLocation(array $locations,
$div_id = 'ip-geoloc-map-multi-locations',
$map_options = NULL,
$map_style = NULL,
$marker_color = NULL,
$visitor_marker = TRUE,
$center_option = 0,
array $center_latlng = [0, 0],
$visitor_location_gps = TRUE) {
if (!isset($map_options)) {
$map_options = IP_GEOLOC_RECENT_VISITORS_MAP_OPTIONS;
}
$marker_directory = $this->fileUrlGenerator->generateAbsoluteString(ip_geoloc_marker_directory());
$marker_dimensions = explode('x', ip_geoloc_marker_dimensions());
$marker_width = (int) $marker_dimensions[0];
$marker_height = (int) $marker_dimensions[1];
$ip_geoloc_marker_anchor_pos = $this->config->get('ip_geoloc_marker_anchor_pos') ? $this->config->get('ip_geoloc_marker_anchor_pos') : 'bottom';
switch ($ip_geoloc_marker_anchor_pos) {
case 'top':
$marker_anchor = 0;
break;
case 'middle':
$marker_anchor = (int) (($marker_height + 1) / 2);
break;
default:
$marker_anchor = $marker_height;
}
ip_geoloc_debug(t('IPGV&M: passing the following to Google Maps:'));
ip_geoloc_debug(t('- map options %options:', ['%options' => $map_options]));
ip_geoloc_debug(t('- center option: @option', ['@option' => $center_option]));
ip_geoloc_debug(t('- visitor marker: %marker', ['%marker' => $visitor_marker]));
ip_geoloc_debug(t('- use GPS: @gps', ['@gps' => (bool) $visitor_location_gps]));
ip_geoloc_debug(t('- visitor location fallback: (@lat, @lng)', [
'@lat' => empty($center_latlng[0]) ? '-' : $center_latlng[0],
'@lng' => empty($center_latlng[1]) ? '-' : $center_latlng[1],
]));
ip_geoloc_debug(t('- marker directory : %dir', ['%dir' => $marker_directory]));
ip_geoloc_debug(t('- marker dimensions : w@w x h@h px, anchor: @a px',
['@w' => $marker_width, '@h' => $marker_height, '@a' => $marker_anchor]));
ip_geoloc_debug(t('- @count locations found', ['@count' => count($locations)]));
// Locations transferred to JS via the settings array aren't refreshed in
// AJAX contexts. That's why we are doing it this messy way. @todo: refactor.
//@TODO Review how to migrate this
$output = "\n<script>\nif (typeof(ip_geoloc_locations) === 'undefined') {\n ip_geoloc_locations = new Array();\n}\n";
$output .= "ip_geoloc_locations['$div_id'] = [\n";
$illegal_chars = ["\n", "\r"];
foreach ($locations as $location) {
if (isset($location->type) && $location->type != 'point') {
continue;
}
// Balloon text must not have newlines, use <br/> instead.
$balloon_text = NULL;
if (!empty($location->balloon_text)) {
$balloon_text = str_replace($illegal_chars, ' ', $location->balloon_text);
$balloon_text = addslashes($balloon_text);
}
$output .= '{"type":"point"' .
(empty($location->marker_color) ? '' : ',"marker_color":"' . $location->marker_color . '"') .
(empty($balloon_text) ? '' : ',"balloon_text":"' . $balloon_text . '"') .
(empty($location->open) ? '' : ',"open":1') .
',"latitude":' . $location->latitude .
',"longitude":' . $location->longitude .
"},\n";
$color = empty($location->marker_color) ? t('default') . " [$marker_color]" : $location->marker_color;
$coords = isset($location->latitude) ? $location->latitude . ', ' . $location->longitude : '';
$msg = '- ' . t('marker') . " $color ($coords)<br/>$balloon_text<br/>";
ip_geoloc_debug($msg);
}
$output .= "];\n</script>\n";
$settings = [
'ip_geoloc_multi_location_map_div' => $div_id,
'ip_geoloc_multi_location_map_options' => Json::decode($map_options),
'ip_geoloc_multi_location_center_option' => (int) $center_option,
'ip_geoloc_multi_location_center_latlng' => $center_latlng,
'ip_geoloc_multi_location_visitor_marker' => $visitor_marker,
'ip_geoloc_multi_location_visitor_location_gps' => $visitor_location_gps,
'ip_geoloc_multi_location_marker_directory' => $marker_directory,
'ip_geoloc_multi_location_marker_width' => (int) $marker_width,
'ip_geoloc_multi_location_marker_height' => (int) $marker_height,
'ip_geoloc_multi_location_marker_anchor' => (int) $marker_anchor,
'ip_geoloc_multi_location_marker_default_color' => $marker_color,
'ip_geoloc_locations' => $locations,
];
if (!isset($map_style)) {
$map_style = IP_GEOLOC_MAP_DIV_DEFAULT_STYLE;
}
$map_placeholder = "<div id='$div_id'" . (empty($map_style) ? '' : " style='$map_style'") . '></div>';
$items_array = [
'#theme' => 'ip_geoloc_map',
'#ip_geoloc_output_map_multi_location' => [
'#markup' => Markup::create($map_placeholder)
],
];
$items_array ['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => $output,
],
];
$items_array ['#attached']['library'][] = 'ip_geoloc/multi_location_js_style_map';
$items_array ['#attached']['drupalSettings'] = $settings;
return \Drupal::service('renderer')->render($items_array);
}
/**
* Uses AJAX to return in $_POST info about the visitor's current location.
*
* Note: this function will result in the browser prompting the visitor to share
* their location, which they may or may not accept.
*
* This works via an asynchronous javascript call, so the result is not
* immediately available on return from this function, hence the $menu_callback.
* Upon page load the included javascript will, when ready, instigate an AJAX
* call to the $menu_callback, which should invoke a function to pull the
* lat/long and address values out of the $_POST variable.
* See ip_geoloc_current_location_ajax_recipient() for an example.
*
* Note: will result in a HTTP error 503 when the site is in maintenance mode,
* as in maintenance mode menu items are not available.
*/
public function getCurrentLocation($menu_callback = NULL, $reverse_geocode = NULL, $refresh_page = NULL) {
$config = \Drupal::config('ip_geoloc.settings');
$throbber_text = $this->config->get('ip_geoloc_throbber_text2');
if (empty($throbber_text)) {
$throbber_text = IP_GEOLOC_THROBBER_DEFAULT_TEXT;
}
if ($throbber_text != '<none>') {
$this->messenger->addMessage(Xss::filterAdmin($throbber_text));
}
$this->ipGeolocSession->setSessionValue('last_position_check', time());
$this->ipGeolocSession->setSessionValue('position_pending_since', microtime(TRUE));
if (!isset($menu_callback)) {
$base_url = \Drupal::request()->getSchemeAndHttpHost();
// @TODO review this as the clean url variable was removed
// $menu_callback = "$base_url/" . (\Drupal::state()->get('clean_url', 0) ? 'js/ip_geoloc/current_location' : '?q=js/ip_geoloc/current_location');
$menu_callback = "$base_url/" . 'js/ip_geoloc/current_location';
}
if (!isset($reverse_geocode)) {
$reverse_geocode = $this->config->get('ip_geoloc_visitor_reverse_geocode') ? $this->config->get('ip_geoloc_visitor_reverse_geocode') : TRUE;
}
if (!isset($refresh_page)) {
$refresh_page = $this->config->get('ip_geoloc_page_refresh') ? $this->config->get('ip_geoloc_visitor_reverse_geocode') : TRUE && !$this->routerAdminContext->isAdminRoute();
}
$settings = [
'ip_geoloc_menu_callback' => $menu_callback,
'ip_geoloc_refresh_page' => $refresh_page,
'ip_geoloc_reverse_geocode' => $reverse_geocode,
];
return $settings;
}
/**
* Returns the location details associated with the supplied IP address.
*
* Performs a lookup in IPGV&M's own database to see if the supplied IP
* address has visited already and if so returns their location details (as an
* array). If the IP address is not yet in the IP geolocation database, then
* retrieve lat/long using either Smart IP or GeoIP API (if enabled) and
* reverse-geocode the lat/long (if the Google Maps service is enabled) into a
* location. If the third argument is TRUE, then store the new location.
*
* @param string $ip_address
* The IP address to locate.
* @param bool $resample
* if set to TRUE, ignore any existing location data for this
* IP address and retrieve the latest.
* @param bool $store
* if TRUE, store the new or resampled location on the db.
* @param bool|null $reverse_geocode
* applies only when the supplied IP address is not
* yet on the database or $resample=TRUE; use TRUE, FALSE or NULL; TRUE will
* produce greater detail in the location returned; if NULL or omitted the
* value is taken from the tick box on the IP Geolocation configuration page.
* Reverse-geocoding is subject to a Google-imposed limit of 2500 calls per
* day from the same server IP address.
*
* @return array
* location as an array
*/
public function getLocationByIp($ip_address, $resample = FALSE, $store = FALSE, $reverse_geocode = NULL) {
$location = $resample ? NULL : $this->database->query('SELECT * FROM {ip_geoloc} WHERE ip_address = :ip_address', [':ip_address' => $ip_address])->fetchAssoc();
if (empty($location)) {
$location = ['ip_address' => $ip_address];
if (ip_geoloc_use_smart_ip_if_enabled($location) || ip_geoloc_use_geoip_api_if_enabled($location)) {
if (!isset($reverse_geocode)) {
$reverse_geocode = $this->config->get('ip_geoloc_google_to_reverse_geocode') ? $this->config->get('ip_geoloc_google_to_reverse_geocode') : FALSE;
}
if ($reverse_geocode && isset($location['latitude']) && isset($location['longitude'])) {
if ($google_address = $this->reverseGeocode($location['latitude'], $location['longitude'])) {
// Should we clear out whatever Smart IP or GeoIP put in the $location
// to avoid fields contradicting eachother? Eg. Google normally
// returns 'locality', whereas Smart IP and GeoIP return 'city'.
// $location = array('ip_address' => $ip_address);.
ip_geoloc_flatten_google_address($google_address, $location);
}
}
if ($store) {
// Calls drupal_alter().
ip_geoloc_store_location($location);
}
else {
//TODO: Migrate drupal alter
//drupal_alter('get_ip_geolocation', $location);
}
}
}
return $location;
}
/**
* Returns the formatted address reverse-geocoded from the supplied lat,long.
*
* See the CALLER BEWARE note at ip_geoloc_reverse_geocode().
*
* @param mixed $latitude
* E.g. "-37.87" or -37.87.
* @param mixed $longitude
* E.g. "144.98" or 144.98.
* @param string $lang
* Optional language specification as a two-letter code, e.g. 'ja' (Japanese).
*
* @return string
* Formatted address component as received from Google or empty string.
*/
public function getAddress($latitude, $longitude, $lang = NULL) {
$google_address = $this->reverseGeocode($latitude, $longitude, $lang);
return !isset($google_address['formatted_address'])? $google_address['formatted_address'] : '';
}
/**
* Uses the Google webservice to retrieve address information based on lat/long.
*
* Effectively makes calls of this form:
* http://maps.googleapis.com/maps/api/geocode/json?sensor=false&latlng=-37,144
*
* CALLER BEWARE:
* This is a server-side, as opposed to client-side call. If you want to call
* this function repeatedly, remember that Google imposes a limit of 2500 calls
* from the same IP address per 24 hours. It may return an OVER_QUERY_LIMIT
* response.
*
* @param string|double $latitude
* E.g. "-37.87" or -37.87.
* @param string|double $longitude
* E.g. "144.98" or 144.98.
* @param string $lang
* Optional language specification as a two-letter code, e.g. 'ja' (Japanese).
*
* @return array|bool
* Array of address components as received from Google or FALSE.
*/
public function reverseGeocode($latitude, $longitude, $lang = NULL) {
if (empty($latitude) || empty($latitude)) {
$this->messenger->addMessage(t('IPGV&M: cannot reverse-geocode to address as no lat/long was specified.'), 'warning');
return FALSE;
}
$query_start = microtime(TRUE);
$url = IP_GEOLOC_GOOGLE_MAPS_SERVER . "?latlng=$latitude,$longitude";
if (!empty($lang)) {
$url .= "&language=$lang";
}
$client = \Drupal::httpClient();
$options = [
'connect_timeout' => 30,
'headers' => array(
'Content-Type' => 'application/json',
),
];
try {
$response = $client->request('GET', $url, $options);
} catch (RequestException $e) {
$msg_args = [
'%url' => $url,
'@code' => $response->code,
'%error' => $response->error,
];
$this->messenger->addMessage(t('IPGV&M: the HTTP request %url returned the following error (code @code): %error.', $msg_args), 'error');
$this->logger->get('IPGV&M')->error('Error (code @code): %error. Request: %url', $msg_args);
return FALSE;
}
$data = Json::decode((string) $response->getBody());
if ($data['status'] == 'OVER_QUERY_LIMIT') {
$msg_args = ['%url' => $url];
if ($this->currentUser->hasPermission('administer site configuration')) {
$this->messenger->addMessage(t('IPGV&M: Server is over its query limit. Request: %url', $msg_args), 'error');
}
$this->logger->get('IPGV&M')->error('Server is over its query limit. Request: %url', $msg_args);
return FALSE;
}
if ($data['status'] == 'ZERO_RESULTS' || !isset($data['results'][0])) {
$msg_args = ['@protocol' => $response->protocol, '%url' => $url];
$this->messenger->addMessage(t('IPGV&M: the @protocol request %url succeeded, but returned no results.', $msg_args), 'warning');
$this->logger->get('IPGV&M')->error('No results from @protocol request %url.', $msg_args);
return FALSE;
}
if ($data['status'] != 'OK') {
$msg_args = ['%url' => $url, '%error' => $data['status']];
$this->messenger->addMessage(t('IPGV&M: unknown error %error. Request: %url..', $msg_args), 'error');
$this->logger->get('IPGV&M')->error('Unknown error %error. Request: %url.', $msg_args);
return FALSE;
}
$google_address = $data['results'][0];
if (empty($google_address['formatted_address'])) {
$msg_args = ['@lat' => $latitude, '@long' => $longitude];
ip_geoloc_debug(t('IPGV&M: (@lat, @long) could not be reverse-geocoded to a street address.', $msg_args), 'warning');
$this->logger->get('IPGV&M')->warning('(@lat, @long) could not be reverse-geocoded to a street address..', $msg_args);
}
else {
$sec = number_format(microtime(TRUE) - $query_start, 1);
$msg_args = [
'@lat' => $latitude,
'@long' => $longitude,
'%sec' => $sec,
'%address' => $google_address['formatted_address'],
];
ip_geoloc_debug(t('IPGV&M: %address reverse-geocoded from (@lat, @long) in %sec s.', $msg_args));
$this->logger->get('IPGV&M')->info('%address reverse-geocoded from (@lat, @long) in %sec s.', $msg_args);
}
return $google_address;
}
/**
* Return the visitor's location as currently stored in the session.
*
* @return array
* Lat/Lon array from SESSION
*/
public function getVisitorLocation() {
$location = $this->ipGeolocSession->getSessionValue('location');
// @ TODO migrate drupal alter
// drupal_alter('ip_geoloc_get_visitor_location', $location);
return $location;
}
/**
* Calculate the center of the supplied locations using one of two algorithms.
*
* The first algorithm returns the center of the rectangle whose horizontal
* sides pass through the top and bottom locations in the set, while its
* vertical sides pass through the left-most and right-most locations.
*
* The second algorithm returns the center of gravity of all supplied locations.
* The second algorithn is therefore sensitive to location clusters. This may
* be what you want, or it may be what you want to avoid.
*
* @param array $locations
* Array of location objects each with latitude and longitude.
* @param bool $center_of_gravity
* If TRUE use the center of gravity algorithm.
*
* @return array
* containing latitude and longitude of the center
*/
public function centerOfLocations(array $locations, $center_of_gravity = FALSE) {
if (empty($locations)) {
return [NULL, NULL];
}
if ($center_of_gravity) {
// Because longitude turns back on itself, cannot simply average coords.
$count = 0;
$x = $y = $z = 0.0;
foreach ($locations as $location) {
if (isset($location->lon)) {
$lng = $location->lon;
$lat = $location->lat;
}
elseif (isset($location->longitude)) {
$lng = $location->longitude;
$lat = $location->latitude;
}
else {
continue;
}
$lng = deg2rad($lng);
$lat = deg2rad($lat);
// Convert to Cartesian coords and total the 3 dimensions independently.
$x += cos($lat) * cos($lng);
$y += cos($lat) * sin($lng);
$z += sin($lat);
$count++;
}
$x /= $count;
$y /= $count;
$z /= $count;
$center_lat = atan2($z, sqrt($x * $x + $y * $y));
$center_lng = atan2($y, $x);
return [rad2deg($center_lat), rad2deg($center_lng)];
}
// Alternative method based on top & bottom lat and left & right lon.
$top = $bottom = $left = $right = NULL;
foreach ($locations as $location) {
if (isset($location->lon)) {
$lng = $location->lon;
$lat = $location->lat;
}
elseif (isset($location->longitude)) {
$lng = $location->longitude;
$lat = $location->latitude;
}
else {
continue;
}
if (!isset($top) || $lat > $top) {
$top = $lat;
}
if (!isset($bottom) || $lat < $bottom) {
$bottom = $lat;
}
if (!isset($left) || $lng < $left) {
$left = $lng;
}
if (!isset($right) || $lng > $right) {
$right = $lng;
}
}
if (!isset($top) || !isset($left)) {
return [NULL, NULL];
}
$center_lat = 0.5 * ($top + $bottom);
$center_lng = 0.5 * ($left + $right);
if ($right - $left > 180) {
// If the angle between right and left is greater than 180, then averaging
// is still ok, provided we flip over to the opposite end of the world.
$center_lng = ($center_lng > 0.0) ? $center_lng - 180.0 : $center_lng + 180.0;
}
return [$center_lat, $center_lng];
}
/**
* Returns the distance (in meters) between two points on the earth's surface.
*
* The points are defined by their lat/long coordinates. If the second point is
* omitted, the current visitor's location is used, as taken from their session
* data.
*
* @param array $location
* Must contain 'latitude' and 'longitude' keys and values.
* @param string $ref_location
* If an array, must contain 'latitude' and 'longitude' keys and values,
* otherwise defaults to ip_geoloc_get_visitor_location()
*
* @return float
* distance in meters.
*/
public function distance(array $location, $ref_location = 'current visitor') {
if (!is_array($ref_location)) {
$ref_location = $this->getVisitorLocation();
}
if (empty($ref_location)) {
return '?';
}
if (is_numeric($location['longitude']) && is_numeric($location['latitude']) && is_numeric($ref_location['longitude']) && is_numeric($ref_location['latitude'])) {
return ip_geoloc_earth_distance($location['longitude'], $location['latitude'], $ref_location['longitude'], $ref_location['latitude']);
}
return '?';
}
/**
* Returns the distance between two points on the earth's surface.
*
* The points are defined by their lat/long coordinates.
*
* Gratefully copied from the http://drupal.org/project/location module, thus
* ensuring compatibility of results.
*
* @param float $longitude1
* Must be in range -180..180.
* @param float $latitude1
* Must be in range -90..90.
* @param float $longitude2
* Must be in range -180..180.
* @param float $latitude2
* Must be in range -90..90.
*
* @return float
* Distance between the two points in meters.
*
* @see http://en.wikipedia.org/wiki/Great-circle_distance
*/
public function earthDistance($longitude1, $latitude1, $longitude2, $latitude2) {
$long1 = deg2rad($longitude1);
$lat1 = deg2rad($latitude1);
$long2 = deg2rad($longitude2);
$lat2 = deg2rad($latitude2);
// $long_factor = cos($long1) * cos($long2) + sin($long1) * sin($long2);
// This is identical to this $long_factor = cos($long1 - $long2).
$long_factor = cos($long1 - $long2);
$cosangle = cos($lat1) * cos($lat2) * $long_factor + sin($lat1) * sin($lat2);
$radius = ip_geoloc_earth_radius(0.5 * ($latitude1 + $latitude2));
$distance = acos($cosangle) * $radius;
/*
if ($distance < 1000) {
// see http://en.wikipedia.org/wiki/Haversine_formula
$sinlat = sin(0.5*($lat1 - $lat2));
$sinlong = sin(0.5*($long1 - $long2));
$sinangle = $sinlat*$sinlat + cos($long1)*cos($long2)*$sinlong*$sinlong;
$distance = 2.0 * asin(sqrt($sinangle)) * $radius;
}
*/
return $distance;
}
/**
* Get radius of the Earth at a given latitude.
*
* @param float $latitude
* The latitude for which to calculate Earth's radius.
*
* @return float
* The radius of Earth at the given latitude
*/
public function earthRadius($latitude) {
$lat = deg2rad($latitude);
$x = cos($lat) / $this->earthRadiusSemimajor();
$y = sin($lat) / $this->earthRadiusSemiminor();
return 1.0 / (sqrt($x * $x + $y * $y));
}
/**
* Get semimajor radius.
*/
public function earthRadiusSemimajor() {
return 6378137.0;
}
/**
* Get semiminor radius.
*/
public function earthRadiusSemiminor() {
return (ip_geoloc_earth_radius_semimajor() * (1.0 - ip_geoloc_earth_flattening()));
}
/**
* Flatten the earth.
*/
public function earthFlattening() {
return (1.0 / 298.257223563);
}
/**
* Return a random point within the circle centered on the supplied location. From: http://stackoverflow.com/a/5838991/5350316 .
*
* @param array $location
* Holding 'lat' and 'lon' entries.
* @param float $radius_km
* Expressed in meters, should be greater than zero.
*/
public function addRandomDisplacement(array &$location, $radius_km) {
$a = ip_geoloc_random(0, 1);
$b = ip_geoloc_random(0, 1);
if ($b < $a) {
// Swap.
$c = $a;
$a = $b;
$b = $c;
}
$angle = 2 * pi() * $a / $b;
// Approximate km to degrees conversion
// See http://stackoverflow.com/questions/1253499/simple-calculations-for-working-with-lat-lon-km-distance
$radius = $radius_km / 111000;
$location['lat'] += $b * $radius * cos($angle);
$location['lon'] += $b * $radius * sin($angle);
}
/**
* Generate a random number uniformly distributed between supplied limits.
*
* @param float $min
* Pending description.
* @param float $max
* Pending description.
*
* @return float
* Pending description.
*
* @ee http://php.net/manual/en/function.mt-getrandmax.php
*/
public function ipGeoLocRandom($min = 0, $max = 1) {
return $min + mt_rand() / mt_getrandmax() * ($max - $min);
}
}
