Карты OpenStreetMap (OSM) представляют собой хорошую альтернативу таким популярным решениям, как Яндекс Карты и Google Maps. Их главное преимущество это, конечно, open source. Для работы с этими картами не нужно нигде регистрироваться, получать API ключ или подвязывать банковскую карту. Хотя движок под эти карты все же несколько устарел по сравнению с конкурентами, однако применительно к миру 1С это даже может стать неким плюсом, так как вам не нужно будет беспокоиться о том, сможет ли ваша версия платформы "потянуть" отображение этих карт, а такая проблема может у вас возникнуть, особенно при работе с Google картами и Яндекс Картами версии 3.
Теперь приступим к делу. Поставим перед собой следующую задачу, которую решим в рамках этой статьи с помощью средств OSM: необходимо создать обработку, в форме которой будет отображаться карта, спозиционированная по умолчанию в центре города, где работает наш клиент. При нажатии на карту в той или иной точке на ней должен установиться специальный маркер. Маркеров можно установить сразу несколько, после чего необходимо построить и вывести на карту оптимальный маршрут проезда между этими точками.
Вывод карты
Создаем новую внешнюю обработку, в которую добавляем макет с типом "HTML документ". Вся логика работы с картами будет прописана именно здесь. Для начала поместим сюда следующий текст HTML:
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenStreetMap</title>
<style>
/* В блоке style мы разместим все CSS стили для корректного и красивого отображения на карте всех элементов*/
#map {
height: 100%;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
// Здесь мы разместим все переменные и функции на языке JavaScript, необходимые для работы с картами и взаимодействия с 1С
</script>
</body>
</html>
Это всего лишь заготовка нашего документа с пустым div с идентификатором "map", внутри которого и будет чуть позже размещена наша карта. Тут нужно обратить внимание на два блока с тегами style и script. Внутри тега style мы поместим все наши стили CSS, необходимые для корректного и более-менее симпатичного отображения на карте всех интересующих на элементов. Так, прямо сейчас мы разместили там две инструкции, благодаря которым наша карта в будущем будет отображаться на полный экран или, если точнее, то на все HTML поле, которое мы разместим позже на форме. В теге же script мы разместим все функции и переменные на языке JavaScript, которые нам понадобятся не только для работы с картами, но и для последующего взаимодействия с 1С.
Приступим, собственно, к инициализации карты OSM. Для отрисовки карты OSM используется библиотека Leaflet. Чтобы подключить ее к нашему HTML документу необходимо поместить в тег head специальные ссылки на сторонние файлы CSS и JS от Leaflet, а в теге scripts прописать код инициализации карты. Изменения в тексте HTML обрамлены комментариями.
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenStreetMap</title>
<!-- ++ -->
<link
rel="stylesheet"
href="/redirect.php?url=aHR0cHM6Ly91bnBrZy5jb20vbGVhZmxldEAxLjkuNC9kaXN0L2xlYWZsZXQuY3Nz"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<!-- -- -->
<style>
#map {
height: 100%;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
// ++
const map = L.map("map", { attributionControl: false }).setView(
[52.0317, 113.501],
13
);
const tiles = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
maxZoom: 19,
}
).addTo(map);
// --
</script>
</body>
</html>
Чуть подробнее остановимся на инициализации карты.
const map = L.map("map", { attributionControl: false }).setView(
[52.0317, 113.501],
13
);
const tiles = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
}).addTo(map);
Объект L был создан автоматически благодаря ссылкам Leaflet, которые мы включили в заголовки HTML документа. Теперь мы используем его для создания карты внутри div с идентификатором "map" и сразу задаем центр нашей карты (setView) на координатах города клиента (массив с широтой и долготой [52.0317, 113.501]), а также устанавливаем зум на 13 единиц.
После этого мы создаем слой карты OSM (tileLayer), который и подвязываем к нашей карте Leaflet.
Достаточно переключиться на вкладку "Просмотр" HTML документа, чтобы убедиться, что все сделано правильно.
Установка маркеров с самописными иконками
Добавим в наш пример возможность добавления нескольких маркеров одновременно. Кроме того, немного усложним задачу и будем выводить не стандартную иконку маркера от Leaflet, а свою собственную.
Сначала создаем переменную массива "markers", где будем хранить координаты всех наших маркеров, а также переменную markerIcon для нашего самописного маркера.
let markers = [];
const markerIcon = new L.DivIcon({
className: "marker",
html: `<img src="marker.png" style="width:20px; height: 20px"/>`,
iconSize: [35, 35],
});
Как видно, маркер использует класс CSS "marker". Добавим и его описание в тег style.
...
.marker {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
font-size: medium;
font-weight: 700;
border-radius: 50%;
background-color: #fff;
border: solid 3px;
border-color: #5158bb;
color: #5158bb;
}
...
Кроме того, в маркере используется картинка "marker.png". На самом деле это всего лишь заглушка, так как в рамках 1С мы не можем подключать сторонние локальные файлы в HTML документ. Поэтому внутри обработки мы позже программно заменим упоминание этой картинки на двоичные данные. Картинку же нашей иконки мы заранее добавим в обработку в качестве макета двоичных данных.
Теперь добавляем событие клика мышкой на карту (onMapClick) - при каждом клике мы будем добавлять на карту новый маркер с нашей иконкой, а также добавлять этот маркер в массив markers.
Кроме этого мы добавим две функции "getLastMarkerLocation" и "removeAllMarkers". Первая функция будет возвращать координаты последнего установленного на карту маркера, а вторая удалит все маркеры с карты. Обе эти функции мы будем вызывать из 1С.
function onMapClick(e) {
addMarker(e.latlng);
}
function addMarker(latlng) {
const marker = L.marker(latlng, { icon: markerIcon }).addTo(map); // Создаем новый маркер
map.panTo(marker.getLatLng()); // Центрируем карту на маркере
markers.push(marker); // Сохраняем маркер в массиве маркеров
}
map.on("click", onMapClick); // Привязываем функцию onMapClick к карте
function getLastMarkerLocation() {
// Эта функция используется для того, чтобы вернуть координаты последнего созданного маркера в 1С
if (markers.length === 0) return null;
const latlng = markers[markers.length - 1].getLatLng();
return [latlng.lat, latlng.lng];
}
function removeAllMarkers() {
// Маркеры удаляются с карты поочередно внутри цикла
for (i = 0; i < markers.length; i++) {
map.removeLayer(markers[i]);
}
markers = []; // Очищаем массив
}
Как видно из кода, новый маркер с самописной иконкой инициализируется с помощью конструкции L.marker(latlng, { icon: markerIcon }).
Метод же map.panTo() вызывается, чтобы сразу после добавления на карту нового маркера центрирвать на нем всю карту.
Итак, пора приступать к работе непосредственно в 1С. В рамках этой статьи мы не будем подробно останавливаться на всех моментах разработки формы - все это довольно просто, кроме того при желании вы можете скачать прикрепленную к статье обработку и самостоятельно изучить, как она устроена. Сфокусируемся только на самых важных моментах.
Создаем реквизит формы "ПолеHTML" с типом неограниченной строки и выводим его на форму с видом отображения "Поле HTML документа". При создании формы мы получаем текст нашего HTML документа из макета обработки, заменяем в нем заглушку картинки иноки ("marker.png") на двоичные данные нашей картинки, которую предварительно зашифровываем в base64, и присваиваем итоговый текст реквизиту "ПолеHTML".
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ИнициализироватьКарту();
КонецПроцедуры
&НаСервере
Процедура ИнициализироватьКарту()
Значение = РеквизитФормыВЗначение("Объект");
ТекстHTML = Значение.ПолучитьМакет("ШаблонКартыHTML").ПолучитьТекст();
ДвоичныеДанные = Значение.ПолучитьМакет("ДвоичныеДанныеМаркера");
ТекстHTML = СтрЗаменить(ТекстHTML, "marker.png", "data:image/png;base64," + ДвоичныеДанныеВBase64(ДвоичныеДанные));
ПолеHTML = ТекстHTML;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ДвоичныеДанныеВBase64(ДвоичныеДанные)
ЗашифрованнаяКартинка = Base64Строка(ДвоичныеДанные);
ЗашифрованнаяКартинка = СтрЗаменить(ЗашифрованнаяКартинка, Символы.ВК, "");
ЗашифрованнаяКартинка = СтрЗаменить(ЗашифрованнаяКартинка, Символы.ПС, "");
Возврат ЗашифрованнаяКартинка
КонецФункции
После каждого клика на карту мы хотим получать координаты выбранной точки и отображать их в таблице на форме. Для этого у поля HTML документа объявляем событие "ПриНажатии", в котором вызываем упомянутую выше функцию JavaScript "getLastMarkerLocation". Делается это следующим образом.
&НаКлиенте
Процедура ПолеHTMLПриНажатии(Элемент, ДанныеСобытия, СтандартнаяОбработка)
Координаты = Элементы.ПолеHTML.Документ.defaultView.getLastMarkerLocation();
Если Координаты = Неопределено Тогда
Возврат;
КонецЕсли;
ТочкаМаршрута = ТочкиМаршрута.Добавить();
ТочкаМаршрута.Широта = Координаты["0"];
ТочкаМаршрута.Долгота = Координаты["1"];
КонецПроцедуры
Обратите внимание, что в при трансляции возвращаемого значения из функции JavaScript в значение 1С мы получаем не привычный нам массив, а объект с типом "ВнешнийОбъект", для получения значения по индексу, из которого используется не числовое представление индекса, а строковое.
Для удаления всех маркеров с карты вызываем функцию "removeAllMarkers" аналогичным образом.
&НаКлиенте
Процедура ОчиститьМаршрут(Команда)
ТочкиМаршрута.Очистить();
Элементы.ПолеHTML.Документ.defaultView.removeAllMarkers();
КонецПроцедуры
Открываем обработку и проверяем результат:
Построение маршрута между точками
Переходим к самой интересной части задачи, а именно к построению маршрута. Сразу замечу, что при построении и оптимизации маршрута мы не ограничены в нашем инструментарии: можно воспользоваться и Google Routes API, и API от Яндекса. Использование карт OSM не накладывает на нас в этом плане никаких ограничений. Однако в рамках этой задачи мы все же прибегнем к помощи родственного сервиса, а именно Leaflet Routing Machine. Этот инструмент тоже бесплатный и очень дружелюбный к разработчикам, так как прямо из коробки нам предоставляется доступ к демо сервису оптимизации маршрутов OSRM, однако на реальных боевых задачах OSRM нужно разворачивать самостоятельно на отдельном сервисе.
Так же, как и в случае с Leaflet, для работы с Leaflet Routing Machine нужно подключить ссылки CDN в заголовки HTML документа.
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenStreetMap</title>
<link
rel="stylesheet"
href="/redirect.php?url=aHR0cHM6Ly91bnBrZy5jb20vbGVhZmxldEAxLjkuNC9kaXN0L2xlYWZsZXQuY3Nz"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<!-- ++ -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-routing-machine/3.2.12/leaflet-routing-machine.min.js"
integrity="sha512-FW2A4pYfHjQKc2ATccIPeCaQpgSQE1pMrEsZqfHNohWKqooGsMYCo3WOJ9ZtZRzikxtMAJft+Kz0Lybli0cbxQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<link
rel="stylesheet"
href="/redirect.php?url=aHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS9hamF4L2xpYnMvbGVhZmxldC1yb3V0aW5nLW1hY2hpbmUvMy4yLjEyL2xlYWZsZXQtcm91dGluZy1tYWNoaW5lLmNzcw=="
integrity="sha512-eD3SR/R7bcJ9YJeaUe7KX8u8naADgalpY/oNJ6AHvp1ODHF3iR8V9W4UgU611SD/jI0GsFbijyDBAzSOg+n+iQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<!-- -- -->
Теперь добавляем в наш HTML документ новые функции и переменные для работы с маршрутами.
let routingControl;
let markers = [];
let distance = 0;
let time = 0;
let instructions;
function buildRoute() {
// Инициализация объекта маршрутизации routingControl
routingControl = L.Routing.control({
waypoints: markers.map((marker) => marker.getLatLng()),
createMarker: function () {
return null;
},
language: "ru",
lineOptions: {
styles: [{ color: "#5158bb", opacity: 1, weight: 5 }],
},
// Здесь указываем адрес нашего сервиса OSRM. Если не указан, то будет использован демо-сервис
// router: L.Routing.osrmv1({
// serviceUrl: `http://router.project-osrm.org/route/v1/`,
// }),
}).addTo(map);
routingControl.on("routesfound", function (e) {
distance = e.routes[0].summary.totalDistance;
time = e.routes[0].summary.totalTime;
instructions = e.routes[0].instructions;
});
}
function getRouteInfo() {
return JSON.stringify({
distance,
time,
instructions,
});
}
function removeRouting() {
if (routingControl) {
map.removeControl(routingControl);
routingControl = undefined;
distance = 0;
time = 0;
instructions = undefined;
}
}
Подробно остановимся на объекте "routingControl", при инициализации которого и происходит построение и отрисовка оптимального маршрута на карте. В конструктор L.Routing.control передаются следующие значения:
- waypoints - собственно точки, через которые должен проехать маршрут
- createMarker - так как мы используем собственные маркеры, то это свойство конструктора мы переписываем таким образом, чтобы оно возвращало нам пустое значение
-
language - язык текстовых инструкций по маршруту, например, таких как "Поверните направо" или "Продолжайте движение вперед" и т.д.
-
lineOptions - стиль линии маршрута
Подробнее обо всех доступных свойствах объекта можно прочитать в документации.
Кроме того, для объекта определяется событие "routesfound". Оно нужно нам, чтобы после отрисовки маршрута сохранить в соответствующих переменных такую информацию о маршруте, как приблизительное время прохождения, дистанцию, а также подробную инструкцию по прохождению маршрута. Для того чтобы получить эти значения позже в 1С, используется функция "getRouteInfo", причем ответ для удобства возвращается в формате JSON. Функция же "removeRouting" используется, чтобы удалить построенный маршрут с карты.
В 1С же добавляем следующие процедуры и функции, для того чтобы вызвать описанные выше функции JS:
&НаКлиенте
Процедура ПостроитьМаршрут(Команда)
Элементы.ПолеHTML.Документ.defaultView.buildRoute();
КонецПроцедуры
&НаКлиенте
Процедура ПолучитьИнформациюОМаршруте(Команда)
ИнформацияОМаршрутеJSON = Элементы.ПолеHTML.Документ.defaultView.getRouteInfo();
ПрочитатьИнформациюОМаршрутеИзJSON(ИнформацияОМаршрутеJSON);
КонецПроцедуры
&НаКлиенте
Процедура ОчиститьМаршрут(Команда)
// Эту процедуру дополняем в соответствии с последними изменениями.
ТочкиМаршрута.Очистить();
Инструкции.Очистить();
ВремяВПути = 0;
Дистанция = 0;
Элементы.ПолеHTML.Документ.defaultView.removeAllMarkers();
Элементы.ПолеHTML.Документ.defaultView.removeRouting();
КонецПроцедуры
&НаСервере
Функция ПрочитатьИнформациюОМаршрутеИзJSON(ИнформацияОМаршрутеJSON)
ИнформацияОМаршруте = ПолучитьЗначениеИзJSON(ИнформацияОМаршрутеJSON);
ВремяВПути = ИнформацияОМаршруте.time;
Дистанция = ИнформацияОМаршруте.distance;
Инструкции.Очистить();
Для Каждого Структура Из ИнформацияОМаршруте.instructions Цикл
СтрокаИнструкции = Инструкции.Добавить();
СтрокаИнструкции.Улица = Структура.road;
СтрокаИнструкции.Текст = Структура.text;
КонецЦикла;
КонецФункции
&НаСервереБезКонтекста
Функция ПолучитьЗначениеИзJSON(JSON)
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(JSON);
Возврат ПрочитатьJSON(ЧтениеJSON);
КонецФункции
Проверяем результат:
Вместо заключения
Эта статья подошла к концу. Мы вместе прошлись по ряду базовых возможностей OpenStreetMap и Leaflet. Хотя, конечно, функционал Leaflet на этом далеко не заканчивается, тем не менеее надеюсь, что все описанные выше приемы работы вы сможете применить в деле, а в качестве бонуса к статье прилагается готовая обработка. Протестировано на платформе 8.3.23.2040.