Строим маршруты на картах в 1С с помощью OpenStreetMap, OSRM и Leaflet


Краткая статья о том как вывести на карту (в 1С) маршруты с помощью OpenStreetMap, OSRM и Leaflet. По данной системе очень мало примеров, но так как OpenStreetMap является бесплатным сервисом и не требует никаких ключей и регистраций, и является довольно мощным механизмом, решил написать небольшую статью «как это сделать?».
В первую очередь скажу, все намного проще, если вы используете последнюю версию платформы (8.3.14), где есть поддержка практически всех браузеров (IE 11, EDGE, Mozilla), но что делать если у нас не самая свежая платформа, где поддержка только IE 9?

Берем Leaflet API

1. Создаем текстовый общий макет:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<link rel="stylesheet" href="/redirect.php?url=aHR0cHM6Ly91bnBrZy5jb20vbGVhZmxldEAxLjMuMS9kaXN0L2xlYWZsZXQuY3Nz" />
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
</head>
<body>
<div id="map" class="map" style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"></div>
<script type="text/javascript">
var map = L.map('map');
map.setView([&ШиротаЦентр, &ДолготаЦентр], 13);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="/redirect.php?url=aHR0cDovL29zbS5vcmcvY29weXJpZ2h0">OpenStreetMap</a> and &copy; <a href="/redirect.php?url=aHR0cDovL215LnNlcnZlcg==">Тут наша организация</a> contributors'
}).addTo(map);
&КодМаршрута
</script>
</body>
</html>

Версия leaflet указывается актуальная, либо используйте ту, которая в примере.. В примере кода заменяются ссылки на неверные <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" />

В коде HTML присутствует 3 параметра: &ШиротаЦентр, &ДолготаЦентр, &КодМаршрута, которые нам нужно будет заменить на свои, при выводе текста в поле HTML документа. Первые два параметра указывают на центр карты для начального отображения (предлагается ставить координаты первой точки маршрута), третий — сам "код" (скрипт) маршрута (возможно сделать встроенные функции на JS, но мне было лень)).

2. Вторым шагом пишем две процедуры (в общем модуле, где хотите), первая — определение геокодирования адреса (получение координат по представлению адреса), описание сервиса доступно по ссылке Nominatim:

Функция ГеокодироватьАдрес(ТекАдрес) Экспорт

оср = Новый HTTPСоединение("nominatim.openstreetmap.org", , , , , , Новый ЗащищенноеСоединениеOpenSSL());
мАдрес = СтрРазделить(ТекАдрес, " ");
Попытка
Запрос = Новый HTTPЗапрос("/?format=json&q=" + СтрСоединить(мАдрес, "+"));
Ответ = оср.Получить(Запрос);
Исключение
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Ошибка при попытке геокодировать по OSR, адрес: " + ТекАдрес + Символы.ПС + "Описание: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;

ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
Попытка
СтруктАдреса = ПрочитатьJSON(ЧтениеJSON);
Исключение
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Ошибка при попытке геокодировать по OSR, адрес: " + ТекАдрес + Символы.ПС + "Описание: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
Если ТипЗнч(СтруктАдреса) = Тип("Массив") Тогда
Если СтруктАдреса.Количество() = 0 Тогда
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Для адреса: """ + ТекАдрес + """ не обнаружено ни одной геопозиции");
Возврат Неопределено;
ИначеЕсли СтруктАдреса.Количество() > 1 Тогда
ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Для адреса: """ + ТекАдрес + """ обнаружено больше одной геопозиции");
КонецЕсли;
Адрес = СтруктАдреса[0];
Возврат Новый Структура("Адрес,Широта,Долгота", Адрес.display_name, Адрес.lat, Адрес.lon)
КонецЕсли;

КонецФункции

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

вторая — получение точек маршрута от routing machine

Функция ПолучитьМаршрут(МассивАдресов)

оср = Новый HTTPСоединение("router.project-osrm.org", , , , , , Новый ЗащищенноеСоединениеOpenSSL());
мТочки = Новый Массив;
Для каждого Точка Из МассивАдресов Цикл
мТочки.Добавить(Формат(Точка.Долгота, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0") + "," + Формат(Точка.Широта, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0"));
КонецЦикла;
Попытка
Запрос = Новый HTTPЗапрос("/route/v1/driving/" + СтрСоединить(мТочки, ";") + "?overview=false&alternatives=true&steps=true&hints=;");
Ответ = оср.Получить(Запрос);
Исключение
Сообщить("Ошибка при попытке маршутизации по OSR, адрес" + Символы.ПС + "Описание: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
Попытка
СтруктМаршрута = ПрочитатьJSON(ЧтениеJSON);
Исключение
Сообщить("Ошибка при попытке маршутизации по OSR, адрес:" + Символы.ПС + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
Если СтруктМаршрута.Свойство("code") Тогда
Если НРег(СтруктМаршрута.code) = "ok" Тогда
Результат = Новый Структура("Маршрут,АльтМаршрут");
Если СтруктМаршрута.routes.Количество() Тогда
Результат.Маршрут = Новый Массив;
Для каждого Нога Из СтруктМаршрута.routes[0].legs Цикл
Для каждого ШагМаршрута Из Нога.steps Цикл
Для каждого Секция Из ШагМаршрута.intersections Цикл
Результат.Маршрут.Добавить(Новый Структура("Долгота,Широта", Секция.location[0], Секция.location[1]));
КонецЦикла;
КонецЦикла;
КонецЦикла;
Если СтруктМаршрута.routes.Количество() > 1 Тогда
Результат.АльтМаршрут = Новый Массив;
Для каждого Нога Из СтруктМаршрута.routes[0].legs Цикл
Для каждого ШагМаршрута Из Нога.steps Цикл
Для каждого Секция Из ШагМаршрута.intersections Цикл
Результат.АльтМаршрут.Добавить(Новый Структура("Долгота,Широта", Секция.location[0], Секция.location[1]));
КонецЦикла;
КонецЦикла;
КонецЦикла;
КонецЕсли;
КонецЕсли;
Возврат Результат;
Иначе
Возврат Неопределено;
КонецЕсли;
ИначеЕсли СтруктМаршрута.Свойство("message") Тогда
Сообщить("Ошибка при попытке маршутизации по OSR, адрес:" + Символы.ПС + СтруктМаршрута.message);
Возврат Неопределено;
КонецЕсли;

КонецФункции // ПолучитьМаршрут()

МассивАдресов — передаем сюда наш массив, полученный в первой процедуре. Для более оптимальной работы, предлагаю хранить координаты в контактной информации объекта (напр. контрагента), получать их на стадии создания адреса. Да забыл, здесь мы получаем не только основной маршрут но и альтернативный (массив АльтМаршрут в структуре результата), его мы нарисуем другим цветом.

3. Теперь нам осталось только создать на основании наших точек сам JS код, который нарисует на полилинию на карте,  и поставит точки начала и конца маршрута (ов).

Функция СформироватьJavaScriptМаршрута(МассивАдресов, Маршрут = Неопределено, АльтМаршрут = Неопределено) Экспорт

КодМаршрута = "";
Для каждого Адрес Из МассивАдресов Цикл
КодМаршрута = КодМаршрута + "L.marker(["+Формат(Адрес.Широта, "ЧЦ=16; ЧДЦ=12; ЧРД=.; ЧГ=0")+", "+Формат(Адрес.Долгота, "ЧЦ=16; ЧДЦ=12; ЧРД=.; ЧГ=0")+"], {title: '" + Адрес.Адрес + "'}).addTo(map);
| ";
КонецЦикла;
Если НЕ Маршрут = Неопределено Тогда
мМаршрут = Новый Массив;
Для каждого ТочкаМаршрута Из Маршрут Цикл
мМаршрут.Добавить("[" + Формат(ТочкаМаршрута.Широта, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0")+", "+Формат(ТочкаМаршрута.Долгота, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0") + "]");
КонецЦикла;
КодМаршрута = КодМаршрута + "var latlngs = [" + СтрСоединить(мМаршрут, ",") + "];
| var polyline = L.polyline(latlngs, {color: 'red', weight: 5, opacity: 0.7}).addTo(map);
| map.fitBounds(polyline.getBounds());
| ";
КонецЕсли;
Если НЕ АльтМаршрут = Неопределено Тогда
мМаршрут.Очистить();
Для каждого ТочкаМаршрута Из АльтМаршрут Цикл
мМаршрут.Добавить("[" + Формат(ТочкаМаршрута.Широта, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0")+", "+Формат(ТочкаМаршрута.Долгота, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0") + "]");
КонецЦикла;
КодМаршрута = КодМаршрута + "var altlatlngs = [" + СтрСоединить(мМаршрут, ",") + "];
| var altpolyline = L.polyline(altlatlngs, {color: 'blue', weight: 5, opacity: 0.7}).addTo(map);
| map.fitBounds(altpolyline.getBounds());
| ";
КонецЕсли;

Возврат КодМаршрута;

КонецФункции // СформироватьJavaScriptМаршрута()

Первым параметром передаем наш массив (с первой функции), вторым и третьим передаем массивы маршрутов со второй функции

Вот и все, теперь осталось подменить строки параметров в тексте HTML и засунуть его в наше поле карты.

Кому лень разбираться, предлагается тестовая обработка. Не судите строго ))

Тестовая обработка написана под УФ (тестировалось на платформе 8.3.11)

З.Ы. в построении маршрута обнаружился некий недочет — изгибы дорог и повороты (некоторые) почему то не учитываются, надо разбираться…

 

Обновлено 12.02.19 г.

Для более точной прорисовки изгибов и внутриквартальных поворотов, изменяем запрос к ресурсу routing machine:

"?overview=full&alternatives=true&steps=true&geometries=geojson&hints=;"

и немного дорабатываем процедуру обработки ответа:

Функция ПолучитьМаршрут(МассивАдресов)

оср = Новый HTTPСоединение("router.project-osrm.org", , , , , , Новый ЗащищенноеСоединениеOpenSSL());
мТочки = Новый Массив;
Для каждого Точка Из МассивАдресов Цикл
мТочки.Добавить(Формат(Точка.Долгота, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0") + "," + Формат(Точка.Широта, "ЧЦ=15; ЧДЦ=12; ЧРД=.; ЧГ=0"));
КонецЦикла;
Попытка
Запрос = Новый HTTPЗапрос("/route/v1/driving/" + СтрСоединить(мТочки, ";") + "?overview=full&alternatives=true&steps=true&geometries=geojson&hints=;");
Ответ = оср.Получить(Запрос);
Исключение
Сообщить("Ошибка при попытке маршутизации по OSR, адрес" + Символы.ПС + "Описание: " + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
Попытка
СтруктМаршрута = ПрочитатьJSON(ЧтениеJSON);
Исключение
Сообщить("Ошибка при попытке маршутизации по OSR, адрес:" + Символы.ПС + ОписаниеОшибки());
Возврат Неопределено;
КонецПопытки;
Если СтруктМаршрута.Свойство("code") Тогда
Если НРег(СтруктМаршрута.code) = "ok" Тогда
Результат = Новый Структура("Маршрут,АльтМаршрут");
Если СтруктМаршрута.routes.Количество() Тогда
Результат.Маршрут = Новый Массив;
ОсновнойМаршрут = СтруктМаршрута.routes[0];
Если ОсновнойМаршрут.Свойство("geometry") И ОсновнойМаршрут.geometry.coordinates.Количество() Тогда
Для каждого Координаты Из ОсновнойМаршрут.geometry.coordinates Цикл
Результат.Маршрут.Добавить(Новый Структура("Долгота,Широта", Координаты[0], Координаты[1]));
КонецЦикла;
Иначе
Для каждого Нога Из ОсновнойМаршрут.legs Цикл
Для каждого ШагМаршрута Из Нога.steps Цикл
Для каждого Секция Из ШагМаршрута.intersections Цикл
Результат.Маршрут.Добавить(Новый Структура("Долгота,Широта", Секция.location[0], Секция.location[1]));
КонецЦикла;
КонецЦикла;
КонецЦикла;
КонецЕсли;
Если СтруктМаршрута.routes.Количество() > 1 Тогда
Результат.АльтМаршрут = Новый Массив;
Альтернативный = СтруктМаршрута.routes[1];
Если Альтернативный.Свойство("geometry") И Альтернативный.geometry.coordinates.Количество() Тогда
Для каждого Координаты Из Альтернативный.geometry.coordinates Цикл
Результат.АльтМаршрут.Добавить(Новый Структура("Долгота,Широта", Координаты[0], Координаты[1]));
КонецЦикла;
Иначе
Для каждого Нога Из Альтернативный.legs Цикл
Для каждого ШагМаршрута Из Нога.steps Цикл
Для каждого Секция Из ШагМаршрута.intersections Цикл
Результат.АльтМаршрут.Добавить(Новый Структура("Долгота,Широта", Секция.location[0], Секция.location[1]));
КонецЦикла;
КонецЦикла;
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецЕсли;
Возврат Результат;
Иначе
Возврат Неопределено;
КонецЕсли;
ИначеЕсли СтруктМаршрута.Свойство("message") Тогда
Сообщить("Ошибка при попытке маршутизации по OSR, адрес:" + Символы.ПС + СтруктМаршрута.message);
Возврат Неопределено;
КонецЕсли;

КонецФункции // ПолучитьМаршрут()

Обновлена обработка…

21 Comments

  1. mi1man

    «.. З.Ы. в построении маршрута обнаружился некий недочет — изгибы дорог и повороты (некоторые) почему то не учитываются, надо разбираться…»

    используйте rs_overview = «full»

    Reply
  2. Ditron

    (1) Благодарю, проверим…

    Reply
  3. Ditron

    (1) нет, не работает, не так надо, а вот так: &geometries=geojson (в запросе) тогда появится структура geometry а в ней массив с более точными координатами… обновлю статью и обработку… спасибо за пинок ))

    Reply
  4. pahalovo

    (3) А полученный geoJson от OSRM можно отправить в leaflet.js L.geoJSON(data). Очень удобно.

    Reply
  5. Ditron

    (4) да можно и так, там вообще можно много чего, да только некогда разбираться ))

    Reply
  6. qwed557

    Координаты получил, в списке адресов 2 точки, при нажатии построить маршрут выходит сообщение

    Ошибка при попытке маршутизации по OSR, адрес:

    Too Many Requests

    Reply
  7. Ditron

    (8)А перевести на русский не судьба? Сервис бесплатный, соответственно нагрузка большая, переводится как «Слишком много запросов», надеюсь дальше объяснять не надо? П.С. это я вывел как ошибку, а вообще это ответ сервера сервиса…

    Reply
  8. qwed557

    (9)да нах такой сервис, в течении дня ни разу не построил маршрут, сколько не бы не тестил. и что то он бесплатный, пользы от него 0.

    Reply
  9. Ditron

    (10)ну извините, не мой сервис, хотя у меня у двух клиентов подсистема сделана маршрутизации, не жалуются, бывает конечно но не напрягает…

    Reply
  10. Ditron

    (10)там на routing machine есть несколько вариантов получения точек для построения полилинии, попробуйте получать в виде таблицы http://project-osrm.org/docs/v5.7.0/api/?language=cURL#table-service

    Reply
  11. Ditron

    (12)или trip

    Reply
  12. Power_0N

    Можно вопрос? А что значит «В примере кода заменяются ссылки на неверные«?

    Никак не могу понять правильно я интерпретирую фразу или нет: я должен заменить в примере неверные ссылки на верные? А что считается верным?

    Reply
  13. Ditron

    (14)Когда писал статью, и вставки кода делал, ИС почему-то заменял ссылки в коде на какую-то хрень)) В тексте (после кода) я указал то, что должно быть

    Reply
  14. Ditron

    (14)А лучше посмотреть файл макета в примере — обработке )

    Reply
  15. sasha777666

    Эта строчка «map.fitBounds(polyline.getBounds());» масштабирует карту так чтобы вся линия была в области видимости, как её изменить если у меня несколько линий (например 3: polyline1, polyline2, polyline3) ?

    Reply
  16. Ditron

    (17) у меня, все маршруты связаны между собой (т.е. последовательны), соответственно, для карты маршрут один получается, ставлю только маркеры в точках отправления (прибытия). По сути (в вашем случае), я так понимаю, нужно получить баунды каждого маршрута (линии) и из них сделать один баунд по мин и макс, а вот как конкретно, нужно читать АПИ, до этого дело у меня не дошло ))

    Reply
  17. sasha777666

    (19) Спасибо за ссылку, всё получилось. Оставлю тут мож кому пригодится:

    координаты всех точек которые должны попасть в область видимости добавлял в переменную «КодТочекМасштабирования» в виде «[55.790831000000, 37.704207000000],[56.790831000000, 38.704207000000],[59.790831000000, 39.704207000000]» , в конце строчка «map.fitBounds(polyline.getBounds());» была заменена на «map.fitBounds([» + КодТочекМасштабирования + «]);»

    Reply
  18. Ditron

    (20)Отлично!

    Reply
  19. palamars

    А как построить маршрут, где точка финиша совпадает с точкой старта? Ну, типичная задача объехать все магазины и вернуться…

    Reply
  20. Ditron

    Что мешает указать последней точкой равной первой ))

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *