Как-то, около 2х лет назад передо мной одна небольшая, но амбициозная фирма поставила задачу:
"Нам нужно определять расстояние от точки до точки в путевом листе Бухгалтерии автоматически, но, чтобы это стоило "мало деняг".
Естественно, как инженеру, мне было интересно решить данную задачу и посмотреть, а есть ли вообще способы бесплатно или почти бесплатно решить поставленную задачу. Спойлер: решение есть.
API Яндекса и 2gis не рассматривались изначально, так как они платные и, если не ошибаюсь, только API 2Gis в тех годах стоял около 130 к в год . Может сейчас все изменилось. Ну, в общем, они не бесплатные.
Порыскав в поисках решения задачи, я нашел отличный русскоязычный сервис, который позволяет работать с адресами, обрабатывая данные по API. К моему сожалению, он не мог определять расстояние, но мог преобразовать адреса в координаты, а это уже что-то. Сам процесс преобразования адреса в координаты называется "геокодирование".
Название сервиса - "DaData". Чтобы не сочли за рекламу, я не буду рассказывать, как зарегаться на этом сервисе и поучить ключи. Загуглите, там дело 5 минут. Так вот, запрос для определения координат по адресу там стоит 15 копеек, а первые 100 запросов вообще бесплатно. Если еще и сохранять координаты адресов куда-нибудь в накопитель для повторного использования, то можно сэкономить еще больше.
Ну, так как мы с вами инженеры - без кода никуда. Самое приятное в этом сервисе на мой взгляд - то, что предварительно не нужно как-то приводить к единому формату адреса. Сервис сам это делает где-то внутри, и очень хорошо работает с российскими адресами.
Рассматривать пример определения расстояния будем на следующих адресах, которые случайно пришли мне в голову:
- Москва Большая садовая 10
- Москва Новослободская 35 (в обработке специально сделал ошибку, чтобы показать, что адрес принимается почти любой)
Именно в таком формате: без точек, запятых и так далее.
Так, на сервисе мы зарегались и ключики получили.
#Область ВспомогательныеФункции
//Инициализирует HTTP соединение
Функция СоединениеССервисом(АдресСервиса)
SSL = Новый ЗащищенноеСоединениеOpenSSL();
Попытка
Соединение = Новый HTTPСоединение(АдресСервиса,,,,,,SSL);
Исключение
ЗаписьЖурналаРегистрации(СтрШаблон("Ошибка установки соединения с сервисом: %1", АдресСервиса),
УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Соединение = Неопределено;
КонецПопытки;
Возврат Соединение;
КонецФункции
// Функция - Запрос на сервис
//
// Параметры:
// Соединение - HTTPСоединение - Соединение, установленное с сервисом см.СоединениеССервисом
// ЗапросHTTP - HTTPЗапрос - HTTPЗапрос с задаными (необходимыми) заголовками
// Параметры - Строка - Строка в формате JSON
// Метод - Строка - "GET" или "POST"
//
// Возвращаемое значение:
// - HTTPОтвет
//
Функция ЗапросНаСервис(Соединение, ЗапросHTTP, Параметры, Метод)
Если Параметры <> Неопределено Тогда
ЗапросHTTP.УстановитьТелоИзСтроки(Параметры);
КонецЕсли;
Попытка
Если Метод = "GET" Тогда
Ответ = Соединение.Получить(ЗапросHTTP);
Иначе
Ответ = Соединение.ОтправитьДляОбработки(ЗапросHTTP);
КонецЕсли;
Исключение
ТекстОшибки = ОписаниеОшибки();
ЗаписьЖурналаРегистрации("Отправка запроса HTTP завершилась с ошибкой", УровеньЖурналаРегистрации.Ошибка,,,ТекстОшибки);
Ответ = Неопределено;
КонецПопытки;
Возврат Ответ;
КонецФункции
#КонецОбласти ВспомогательныеФункции
// Определяет координаты точки А и точки Б с помощью сервисов Dadata. Адрес необходимо писать в читаемой форме
// Например: "Москва Большая садовая 10", "Большая садовая 10 Москва" и т.п. Российские адреса воспринимает корректно
// В большинстве случаев.
//
// Параметры:
// ТочкаА - Строка - Адрес откуда считаем расстояние
// ТочкаБ - Строка - Адрес докуда считаем расстояние
//
// Возвращаемое значение:
// Структура:
// *КоординатыТочкиА - Структура - Координаты с ключами "Долгота" и "Широта"
// *КоординатыТочкиБ - Структура - Координаты с ключами "Долгота" и "Широта"
//
Функция КоординатыССервисаDaData(ТочкаА, ТочкаБ) Экспорт
КоординатыТочек = Новый Структура("КоординатыТочкиА, КоординатыТочкиБ", "", "");
СоединениеСDadata = СоединениеССервисом("cleaner.dadata.ru");
//Обработаываем как нужно
Если СоединениеСDadata = Неопределено Тогда
Возврат КоординатыТочек;
КонецЕсли;
КоординатыТочкиА = КоординатыТочки(ТочкаА, СоединениеСDadata);
КоординатыТочкиБ = КоординатыТочки(ТочкаБ, СоединениеСDadata);
КоординатыТочек.КоординатыТочкиА = КоординатыТочкиА;
КоординатыТочек.КоординатыТочкиБ = КоординатыТочкиБ;
Возврат КоординатыТочек;
КонецФункции
//Получает координаты Долгота и Широта для одного адреса. (вспомогательная)
//
// Параметры:
// Адрес - Строка - Адрес, у которого необходмо получить координаты
// СоединениеСDadata - HTTPСоединение - Соединение, установленное с сервисом Dadata
//
// Возвращаемое значение:
// - Структура:
// *Долгота - Строка - Долгота
// *Широта - Строка - Широта
//
Функция КоординатыТочки(Адрес, СоединениеСDadata)
КоординатыТочки = Новый Структура("Долгота, Широта", "", "");
ВашТокен = "";
СекретныйКлюч = "";
ЗапросHTTP = Новый HTTPЗапрос();
ЗапросHTTP.АдресРесурса = "api/v1/clean/address";
ЗапросHTTP.Заголовки.Вставить("Content-Type" , "application/json");
ЗапросHTTP.Заголовки.Вставить("Authorization", "Token " + ВашТокен);
ЗапросHTTP.Заголовки.Вставить("X-Secret" , СекретныйКлюч);
ПараметрыЗапроса = СтрШаблон("[ ""%1"" ]", Адрес);
РезультатЗапроса = ЗапросНаСервис(СоединениеСDadata, ЗапросHTTP, ПараметрыЗапроса, "POST");
//Обработайте ошибку, как вам нужно, если код состояния не 200
Если РезультатЗапроса.КодСостояния <> 200 Тогда
ЗаписьЖурналаРегистрации("Ошибка запроса, код состояния: " + РезультатЗапроса.КодСостояния, УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON();
МассивОтвета = Новый Массив;
Попытка
ЧтениеJSON.УстановитьСтроку(РезультатЗапроса.ПолучитьТелоКакСтроку());
МассивОтвета = ПрочитатьJSON(ЧтениеJSON);
Исключение
ЗаписьЖурналаРегистрации("Не удалось прочитать JSON по причине", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
Если МассивОтвета.Количество() = 1 Тогда
КоординатыТочки.Долгота = МассивОтвета[0].geo_lon;
КоординатыТочки.Широта = МассивОтвета[0].geo_lat;
КонецЕсли;
Возврат КоординатыТочки;
КонецФункции
На выхлопе мы с вами получаем координаты точек:
Все, половина дела сделана. Можно расслабиться (нет).
Теперь нам нужно куда-то зарядить эти координаты, чтобы получить расстояние. И тут, где-то на форумах я наткнулся на информацию о некоем открытом проекте - "OSRM", расшифровывается как "Open source routing machine". И вот он как раз делает то, что нам нужно. Сам ресурс находится по адресу: https://project-osrm.org/
У него достаточно полная документация. Не буду вдаваться в детали и подробности, можно посмотреть самим. Здесь я рассказываю конкретно о том, как решалась данная задача.
С этого сервиса, нам нужен метод: route. Метод вызывается "GET" запросом и в строку мы передаем - как координаты двух точек, так и с помощью чего мы планируем двигаться между ними: car , bike or foot. Как можно понять, данный сервис считает расстояние не по прямой, а именно так нам и нужно.
Пришло время закодить эту историю:
// Отправляет запрос на определение расстояния между координатами
//
// Параметры:
// КоординатыТочек - Структура:
// * КоординатыТочкиА - Структура - Координаты с ключами "Долгота" и "Широта"
// * КоординатыТочкиБ - Структура - Координаты с ключами "Долгота" и "Широта
//
// Возвращаемое значение:
// - Структура:
// *Расстояние - Число
//
Функция РасстояниеМеждуКоординатами(КоординатыТочек) Экспорт
РасстояниеМеждуТочками = Новый Структура("Расстояние", 0);
Соединение = СоединениеССервисом("router.project-osrm.org");
ЗапросHTTP = Новый HTTPЗапрос();
//Здесь прописываем строку с параметрами для get запроса.
//https://project-osrm.org/docs/v5.24.0/api/ Тут можно посмотреть документацию. Расстояние можно строить, используя различные способы
//передвижения "car , bike or foot". В данном случае вычислим для машины
ЗапросHTTP.АдресРесурса = СтрШаблон("route/v1/car/%1,%2;%3,%4",
КоординатыТочек.КоординатыТочкиА.Долгота,
КоординатыТочек.КоординатыТочкиА.Широта,
КоординатыТочек.КоординатыТочкиБ.Долгота,
КоординатыТочек.КоординатыТочкиБ.Широта);
РезультатЗапроса = ЗапросНаСервис(Соединение, ЗапросHTTP, Неопределено, "GET");
//Здесь можно обработать код состояния
Если РезультатЗапроса.КодСостояния <> 200 Тогда
//Сделай что-то
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON();
СтруктураОтвета = Новый Структура;
Попытка
ЧтениеJSON.УстановитьСтроку(РезультатЗапроса.ПолучитьТелоКакСтроку());
СтруктураОтвета = ПрочитатьJSON(ЧтениеJSON);
Исключение
ЗаписьЖурналаРегистрации("Не удалось прочитать JSON по причине", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
Если СтруктураОтвета.Свойство("routes") Тогда
РасстояниеМеждуТочками.Расстояние = СтруктураОтвета.routes[0].distance;
КонецЕсли;
Возврат РасстояниеМеждуТочками;
КонецФункции
Никаких ключей, регистраций и т.п. Единственное с чем столкнулись: сервис иногда не совсем, как бы это сказать, доступен. И ничего с этим не поделаешь, придется ждать. Но, вроде как по документации, если правильно помню, можно развернуть эту красоту локально у себя.
Ну и в завершение поделюсь функцией, которая объединяет все вышенаписанное воедино:
#Область Вспомогательное
//Сопоставляет массив входящих условий оператором И.
Функция Конъюкция(Операнды)
Для Каждого Операнд Из Операнды Цикл
Если Операнд <> Истина Тогда
Возврат Ложь;
КонецЕсли;
КонецЦикла;
Возврат Истина;
КонецФункции
#КонецОбласти
// Получает расстояние от точки до точки, через сервисы определения координат и геокодирования
//
// Параметры:
// ТочкаА - Строка - Адрес отправления
// ТочкаБ - Строка - Адрес назначения
//
// Возвращаемое значение:
// - Число - Расстояние между точками
//
Функция РасстояниеОтТочкиАДоТочкиБ(ТочкаА, ТочкаБ) Экспорт
РезультатОбработки = Новый Структура("ЕстьОшибки, ОписаниеОшибки, РезультатЗапроса", Ложь, "", "");
//Сначала получим координаты с сервиса Dadata
КоординатыТочек = КоординатыССервисаDaData(ТочкаА, ТочкаБ);
Операнды = Новый Массив;
Операнды.Добавить(ЗначениеЗаполнено(КоординатыТочек.КоординатыТочкиА.Долгота));
Операнды.Добавить(ЗначениеЗаполнено(КоординатыТочек.КоординатыТочкиА.Широта));
Операнды.Добавить(ЗначениеЗаполнено(КоординатыТочек.КоординатыТочкиБ.Долгота));
Операнды.Добавить(ЗначениеЗаполнено(КоординатыТочек.КоординатыТочкиБ.Широта));
ЗаполненыКоординаты = Конъюкция(Операнды);
//Для примера, тупо проверка на общую заполненость. Можно реализовать более детальную проверку
Если Не ЗаполненыКоординаты Тогда
РезультатОбработки.ЕстьОшибки = Истина;
РезультатОбработки.ОписаниеОшибки = "Не заполнены обязательные координаты";
Возврат РезультатОбработки;
КонецЕсли;
//Теперь получаем расстояние с опенсорсного проекта http://router.project-osrm.org/
РасстояниеМеждуТочек = РасстояниеМеждуКоординатами(КоординатыТочек);
РезультатОбработки.РезультатЗапроса = РасстояниеМеждуТочек.Расстояние;
Возврат РезультатОбработки;
КонецФункции
На выхлопе мы получаем структуру со множеством данных об адресах, а также то, что нам и было нужно - расстояние в метрах.
Если сравнивать с Яндексом, то, можно сказать, что почти "пуля в пулю".
На этом все. Не претендую на гениальность, но вдруг кому-нибудь будут полезны эти изыскания.
Обработку также прикладываю.
Проверено на следующих конфигурациях и релизах:
- Бухгалтерия предприятия, редакция 3.0, релизы 3.1.22.86