Да я тебя по ip вычислю! Геолокация пользователей

Ильдар Сарибжанов | 27.04.2017

Вступление

Сейчас будет неожиданное вступление!

Приготовились? (Дальше каждое слово нужно читать через вдох, чтобы придать глубокий смысл сказанному)

Однажды… Поступил… Заказ… =))

В общем, я не смог придумать оригинальное вступление =)

Мы работаем с разными клиентами, одни приходят и уходят, другие остаются с нами надолго. Вот один из последних к нам и обратился в очередной раз. Его бизнес по продаже некоторого метрологического и измерительного оборудования раскинулся на несколько регионов нашей необъятной страны. И как он сам выразился: «В нашем бизнесе есть менталитет звонить в тот регион, в котором находится заказчик». Таким образом появилась необходимость модуля, который бы определял географию пользователя и в зависимости от этих данных выводил в контактах региональный телефон, ближайший к пользователю.

Что нужно для решения поставленной задачи?

Я решил для начала определить круг необходимостей для задачи:

  1. Мне нужно определять координаты пользователя
  2. Мне нужно как-то связывать координаты пользователя и регион
  3. Мне нужно в зависимости от этого региона выбрать нужный номер телефона

Каждая из частей может быть решена несколькими способами, я попробую описать свой ход мыслей, а ты постарайся не отставать =)

Определение координат пользователя

Если попробовать поискать по такому запросу, то первые варианты отправят тебя на базы ip адресов. На самом деле, это хороший вариант, если не нужна высокая точность. Правда стоит оговориться, «невысокая» может быть очень сильно невысокой. Например, я живу в Муроме, а мой ip чаще всего приписывают Владимиру, т.е. ошибка в 100+ км — это нормальное явление.

Лично мне понравилась реализация базы от этих парней. У них есть несколько постов на хабре, реагируют на обратную связь, предлагают библиотеки для разных языков. В общем, всё круто. Эту базу и решил взять в качестве отправной точки. Не вижу смысла рассказывать как с ней работать, в примерах на сайте всё хорошо расписано.

Хмм.. А что делать если нужна точность выше?

Самым очевидным решением мне показалось использование HTML5 geolocation API. Все современные браузеры (или большинство из них) поддерживают эту возможность, но есть ряд особенностей:

  1. Пользователь должен явно разрешить передавать данные своей геолокации. Думаю, все видели такое окно «разрешить сайту использовать/передавать данные о локации» или как-то так. Это может пугать.
  2. Вытекающее следствие из первого пункта: пользователь может отказаться от передачи своих данных, т.е. использовать только geolocation API плохой вариант.
  3. Google Chrome требует защищенного протокола для передачи информации. Ну это ssl-сертификат, ну такой https, зелененький в адресной строке. Конечно, не проблема поставить бесплатный сертификат. Или проблема?

Меня особенно волновал последний пункт, пользовательский сайт хостится на nic’е, а там только платные сертификаты устанавливаются легко, а вот с Let’s encrypt (ты еще не знаешь, что там можно получить бесплатный ssl сертификат?! Тогда мы идем к тебе!) что-то не очень они дружат, либо я плохо искал. В общем, мне нужно заставить работать HTML5 геолокацию по обычному протоколу. На помощь приходит яндекс API, точнее яндекс.карты. При этом совершенно не обязательно размещать саму карту на странице, она мне не нужна, можно просто определять координаты пользователя.

Самый простейший вариант выглядит так

function get_location_in_browser() {
	ymaps.geolocation.get({
		provider: 'auto'
	}).then(function ( result ) {
		// Где пользователь
		var pos = result.geoObjects.position;

		console.log('lat = ', pos[0]);
		console.log('lon = ', pos[1]);
	});
}

ymaps.ready(get_location_in_browser);

Параметр provider отвечает за метод определения координат пользователя, он так же может работать по ip (по данным яндекса, так в документации написано) или встроенная браузерная (на сколько я понял это и есть HTML5 geolocation). Значение авто выбирает лучший вариант, как он это делает, не знаю, не спрашивай.

Координаты пользователя получили. Ю-ху! Я думал, что это 70% задачи, но ошибся в процентовке. Теперь надо как-то связать координаты пользователя и некий региональный телефон, только количество регионов может быть неопределенно много. Как быть?

Координаты региональных телефонов

Когда размышлял над задачей в голове возникла гениальная мысль, что для связи координат пользователя и строки «Москва» как минимум нужны координаты этой самой Москвы. Но как же определить координаты имея строку? И снова к нам приехал яндекс со своими сервисами, а именно «Геокодирование». Берем строку Москва, и отправляем ее по адресу:

https://geocode-maps.yandex.ru/1.x/?geocode=Москва

В ответ получаем xml ответ, но мне больше нравится json вариант ответа

https://geocode-maps.yandex.ru/1.x/?format=json&geocode=Москва

Сначала для получения ответа использовал file_get_contents, но на боевом сервере он что-то не заработал, какие-то траблы с ssl, поэтому использовал curl

/**
 * Геокодирование строки
 * 
 * @param string $name
 *
 * @return array
 */
function get_goecode_position($name = '')
{
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode=' . $name);
	curl_setopt($ch, CURLOPT_HEADER, 0);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	$region_position = json_decode(curl_exec($ch));
	curl_close($ch);
	
	$region_position = explode(' ',
		$region_position->response->GeoObjectCollection->featureMember[0]->GeoObject->Point->pos);
	
	return array(
		'latitude'  => $region_position[1],
		'longitude' => $region_position[0]
	);
}

Прикол еще в том, что в ответе может быть не один результат, но как правило первым идет наиболее релевантный вариант, поэтому лучше брать его. Но крутость сервиса в том, что можно указать точный адрес, например, Муром, Комсомольская 72, и на выходе получим координаты нашего офиса, это мега круто! Например, если будем говорить в рамках текущей задачи, нужно указать несколько телефонов для одного города. Мысль понятна? Чудо, не правда ли? =)

Теперь у меня есть n+1 точка на карте, где n это все региональные адреса на карте, а +1 координаты пользователя. Едем дальше.

Как связать координаты пользователя и региональные телефоны?

Поупражняемся в математике.

Как связать 2 точки? Провести линию. Как определить номер телефона необходимый пользователю? Провести линии от пользователя до всех региональных телефонов и найти кратчайшее расстояние из полученных. На плоскости в общем случае задача решается теоремой пифагора, но у нас не совсем плоскость и точки, не совсем точки а координаты. Т.е. нам нужно учитывать что расстояние это длинна дуги. Сам осилишь формулу написать? Вот и я не стал заморачиваться, хотя, думаю, если бы поразмышлять пару часиков формулу можно было бы получить. В общем великий интернет дал мне необходимое решение в виде функции.

/**
 * Расстояние между двумя точками
 *
 * Михаил Кобзарев
 *
 * @param $φA - широта, долгота 1-й точки,
 * @param $λA - широта 1-й точки,
 * @param $φB - широта 2-й точки,
 * @param $λB - долгота 2-й точки,
 *
 * @return float
 */
function calculateTheDistance($φA, $λA, $φB, $λB)
{
	$rad = 6372795; // ~Радиус земли
	
	// перевести координаты в радианы
	$lat1  = $φA * M_PI / 180;
	$lat2  = $φB * M_PI / 180;
	$long1 = $λA * M_PI / 180;
	$long2 = $λB * M_PI / 180;
	
	// косинусы и синусы широт и разницы долгот
	$cl1    = cos($lat1);
	$cl2    = cos($lat2);
	$sl1    = sin($lat1);
	$sl2    = sin($lat2);
	$delta  = $long2 - $long1;
	$cdelta = cos($delta);
	$sdelta = sin($delta);
	
	// вычисления длины большого круга
	$y = sqrt(pow($cl2 * $sdelta, 2) + pow($cl1 * $sl2 - $sl1 * $cl2 * $cdelta, 2));
	$x = $sl1 * $sl2 + $cl1 * $cl2 * $cdelta;
	
	// Вычисление расстояния
	$ad   = atan2($y, $x);
	$dist = $ad * $rad;
	
	return round($dist);
}

Поскольку писалось не мной, считаю, что нужно указать авторство, кстати, там есть выкладки каким образом получилась эта функция.

Казалось бы, задача решена, остается все это обернуть в модуль/плагин/расширение и всё готово. Но нужно вернуться к ТЗ. Было сказано, что нужно определять регион пользователя и отдавать ему нужный телефон, в случае, если его региона нет, то выводить телефон центрального офиса. Что это значит? Например, некто зашел на сайт его позиция определилась как Владивосток, и ближайший к нему регион Екатеринбург, а это всего-то 5000 километров, технически, конечно, это ближайший регион, только вот чтобы обсудить детали заказа, клиент не сможет подъехать в офис после обеда. Таким людям надо показывать телефон центрального офиса в Москве.

Соответственно, нужно указывать радиус действия регионального телефона. Собственно, как слышу так и пишу. В настройки добавил еще радиус действия телефона. Если координаты пользователя и регионального телефона попадают в радиус действия этого телефона, то кладем этот телефон и его расстояние в некоторый словарь, а потом выбираем из него телефон с минимальным расстоянием. Profit!

В моем случае настройки получились такими

Так как браузерная геолокация не всегда необходима, и требует дополнительные ресурсы на выполнение себя, решено было добавить настройку, которая будет включать её.

В самых общих чертах задача решена =) Можно, наверное, докрутить еще какие-нибудь фишечки, но я не хочу.

Итоги

Думаю, теперь ты понял почему фраза «Я тебя по ip вычислю» не имеет под собой основания. Вероятно, когда ip’шников было 3 на город, такой фразой можно было пугать.

Чем дальше в лес тем меньше надо думать, всё уже придумано и реализовано до нас. Это я про формулу длины дуги на поверхности шара. Это удобно, но мозг дело такое, чем меньше ты его включаешь, тем меньше он у тебя становится. Не забывай книжки читать. Понял? Какую бы еще мудрость тебе открыть? Не ешь после 18:00, чисти зубы утром и вечером, не обижай младших. Хотя на счет последнего, есть что обсудить, у меня есть младший брат.

Ах, да! Про программирование =) Задача решена, обернул всё это в плагин для WP (Ой, да ладно! Уже даже не интересно слушать про то, что WP — гумно. Всё у него хорошо. И заметь про WP ты узнал только в выводах ;) )

Всем рок!