Евгений Яфаркин
Ведущий разработчик ГК Юзтех
Всем привет! Меня зовут Евгений Яфаркин, я dotnet backend разработчик. В этой статье я хочу поделиться своим опытом решения задач, связанных с отображением данных на геокарте: как самим решением, так и инструментами, которые мы для этого использовали.
В первую очередь, статья будет интересна техническим специалистам. Также, найденное решение, как и решение по визуализации данных, будут интересны владельцам продуктов.
Статья разбита на две большие части:
Проблема с извлечением данных.
Представление этих данных на карте.
Извлечение данных
У нашего заказчика появилась задача: найти решение, с помощью которого партнеры компании смогут выбрать себе место для открытия офиса. Предполагалось, что мы должны реализовать географическую карту, на которой тем или иным способом будут отображаться данные по плотности населения.
Появились вопросы: «Где взять данные по плотности населения и как их отобразить?». Аналитиками было найдено решение — на тот момент эти данные предоставлял сайт реформы ЖКХ. Файл, который мы оттуда скачали, содержал списки домов с различной информацией о них: состояние дома, управляющая компания и т.д. Ключевым было то, что там содержится информация о количестве квартир в доме. Применив определенный коэффициент, мы получили примерное количество жителей.
Примечание: Сейчас скачать такой массив данных с сайта реформы ЖКХ нельзя, возможно лишь получить данные по конкретному дому.
Надо понимать, что это далеко не полные данные: мы получили информацию только о миллионе домов. К примеру, если взять данные из OpenStreetMap (далее — OSM), на территории РФ порядка 8 миллионов домов.
У нас есть данные об адресах домов в виде строки, и о количестве жителей в этом доме. Каким образом всё это поместить на карту? Для этого выполняется операция геокодирования — это механизм преобразования строки адреса в широту и долготу. В целом, геокодирование предоставляют разные сервисы: Google, Яндекс, и др. Но они все платные. Мы нашли Open Source решение — сервис Nominatim. Этот сервис использует данные с карт OpenStreetMap и позволяет выполнить запросы бесплатно, онлайн и без регистрации. Но не очень быстро: порядка одного запроса в секунду. В нашем случае было около миллиона строк с адресами, что могло бы занять прилично времени. Но, так как это Open Source решение, была возможность развернуть сервер Nominatim локально в Docker-контейнере и выполнять запросы к нему. На GitHub вы найдете всю необходимую информацию для настройки развертывания. Первоначальное скачивание и развертывание сервиса у меня заняло около 10 часов и 70 Гб на диске. В дальнейшем, используя АПИ Nominatim, вы сможете выполнять запрос на геокодирование, обратное геокодирование, получать информацию об объектах и так далее.
При помощи скринов покажу небольшую демонстрацию сервиса. Введя в браузере поисковый запрос «Тольятти, ТЦ «Русь на Волге», получаю следующий результат:
Отобразился подходящий вариант под данный запрос. А ниже есть детали найденного ответа:
Открылась карточка объекта и данные этого узла. Это узел ОSM, то есть привязаны атрибуты, теги и тд. Конечно, у разных единиц информации здесь будет разный информационный объем. Нам же интересна строка way 29979750:
Используя эту строку, я выполню запрос в базе данных. Сервис базы у меня развернут локально, то есть запрос выполняется в Docker локально на моей машине. После того, как мы локально развернули Nominatim, он поднимает PostgreSQL с расширениями PostGIS. Так, исходный файл развернулся в базу данных, и это позволяет нам спокойно выполнять запросы, просматривать внутреннее устройство и извлекать разного рода дополнительные данные.
Соответственно, мы нашли наш объект в базе и можем посмотреть содержимое. Мы видим, что это объект типа «Торговый центр», имя этого объекта, определенные экстра теги, адрес и так далее. Что здесь еще может быть интересно? Так как это PostGIS, и DBeaver поддерживает это расширение, то раздел «Геометрия» нам позволяет прямо в
DBeaver посмотреть, как выглядит объект на карте.
Важно понимать, что в аббревиатуре OSM первое слово — Open. Это означает, что данные наполняются силами комьюнити и не всегда могут быть полными, достоверными и корректными. Но в целом, грамотно используя данные по OSM, можно получить большое количество значимой бизнес-информации.
Покажу еще один пример — как можно выбрать список торговых центров:
Здесь первые 10 торговых центров, которые мы выбрали, и по базе можно посмотреть, где они находятся, названия этих торговых центров, и т.д. Но самое главное, что таким же образом можно выбрать другие объекты, например, жилые дома, магазины или светофоры.
Подведу итог: база OSM содержит большой массив информации, которую вы можете использовать, например, в маркетинговых целях. Грамотное использование этой информации поможет облегчить принятие решений для бизнеса.
Итак, на этом этапе мы были готовы формировать данные для получения плотности населения. Была написана утилита, которая разбирала исходные CSV-файлы с сайта реформы ЖКХ и дополняла их координатами домов.
К сожалению, Nominatim не решил всех наших проблем, но сильно уменьшил их количество. В среднем, на моей рабочей машине он выполнял порядка 40 запросов в секунду на геокодинг и позволил закрыть около 80% запросов. Оставшиеся 20% пришлось закрывать с помощью сервиса dadata, используя платный ключ компании.
Итоги по OSM
Использование данных OpenStreetMap через Nominatim поможет вашему бизнесу принимать решения на более уверенной основе. С технической стороны, это позволит закрыть вопросы по геокодингу не прибегая к платным решениям. В целом, качество этого решения ниже, чем платных решений, но оно бесплатное и работает достаточно быстро, качественно и разворачивается автоматически через Docker. С точки зрения бизнеса это действительно гигантский массив данных, которые вы можете использовать для маркетинговых целей.
Визуализация данных на карте
Мы подошли ко второй части — к визуализации полученных данных на геокарте. Для отображения данных мы решили использовать библиотеку H3, которая позволяет создавать гексагоны и накладывать их на карту. Если очень просто, то это такая библиотека, которая позволяет виртуально спроецировать на сферу сетку шестигранников (иногда пятигранников) и потом работать с этой сеткой, причем работать крайне быстро.
Можно сказать, эта библиотека позволяет кластеризовать геокоординаты. Всё, что нужно — это создать объект и передать ему координаты и разрешение. Разрешение — это площадь, которую будет занимать гекс.
На картинке слева приведено сравнение гексов в разрешении 8 (синие) и 9 (красные). В целом, зависимость прямая: чем больше разрешение, тем больше гексов будет помещаться на карте и тем меньше будет площадь каждого из них. Важно находить баланс между разрешением гексов и точностью карты. Понятно, что чем меньше гексагон, тем точнее можно какие-то границы очертить, но тем больше будет количество гексагонов. Вы можете использовать иерархию гексагонов — пример на картинке справа. Это я взял из статьи по использованию H3 на базе PostGIS.
Как происходит кластеризация? При создании гексагона вы передаете координаты. Если они попадают в площадь одного виртуального гекса, то всегда будет создаваться тот же самый гекс. При этом это работает действительно быстро. Гексагон в данной модели представляет собой беззнаковое восьмибайтовое число. Вам достаточно знать это число, чтобы полностью создать гексагон заново. Единственное, в случае JavaScript для передачи этого числа вам надо будет использовать строку для передачи, так как он теряет точность на этих числах.
Вы можете хранить это число в базе или использовать его в коде. Ещё раз, достаточно одного восьмибайтового числа, чтобы получить всю информацию об этом гексагоне: координаты, геометрию и так далее.
Для работы в dotNet я остановился на библиотеке Pocketken — это порт библиотеки на C#, основанный на базе пакета NetTopologySuite. В настоящий момент мы используем минимум функций из этой библиотеки: создание объекта гексагона из его числового представления или из переданных координат, создание гексагонов по определенной дистанции, а также заполнение переданного полигона гексами (то есть, когда необходимо какой-то полигон на карте заполнить гексагонами).
Для работы с геометрией используется NetTopologySuite. Он предоставляет возможность создавать и работать с геометрическим примитивами, преобразовывать их в текстовое представление и обратно, а также выполнять операции объединения и проверки на вхождение объектов. Гексагоны очень удобно использовать для редактирования карты — вам достаточно сделать использование гексагона для привязки к определенному объекту, например, карт покрытия офисами. И это удобно хранить как в базе, так и отображать на UI и редактировать пользователю.
К примеру, вам надо сделать территорию доставки пиццы по районам города в зависимости от цены. Можно рисовать вручную эти полигоны, чтобы они не пересекались. Эту информацию надо как-то хранить в базе, проверять попадание точек, определенных пересечений и так далее. Либо вы можете сделать просто набор гексов, которые однозначно не будут перекрываться и которые очень удобно будет на той же карте задать. Это быстро, удобно хранить, удобно работать и использовать, достаточно широко распространена поддержка. Это была одна из главных причин, по которой мы выбрали гексагоны.
Хочу порекомендовать, на мой взгляд, достаточно хорошую статью про анализ данных с помощью H3 и Python. В этой статье неплохо разбирается, почему стоит использовать гексагоны, возможности библиотеки и описание практического применения.
Далее хочу показать вам пример кода, который собирает объекты недвижимости, складывает их в словарь и суммирует количество записей в базу:
using H3;
using H3.Extensions;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO;
using Newtonsoft.Json;
const int resolution = 8;
const double minLat = 55.0;
const double maxLat = 56.0;
const double minLon = 37.0;
const double maxLon = 38.0;
var r = new Random();
var cnt = r.Next(100, 10_000);
// generate
var someValues = new List<SomeData>();
for (var i = 0; i < cnt; i++) {
var someValue = new SomeData(
minLat + r.NextDouble() * (maxLat - minLat),
minLon + r.NextDouble() * (maxLon - minLon),
r.Next(1, 1000));
someValues.Add(someValue);
}
// build
var hexagons = new Dictionary <H3Index, long>();
foreach(var someValue in someValues) {
var hex = H3Index.FromPoint(new Point(someValue.Longitude, someValue.Latitude), resolution);
if (hexagons.ContainsKey(hex)) {
hexagons[hex] += someValue.Value;
} else {
hexagons.Add(hex, someValue.Value);
}
}
// output
var fc = new FeatureCollection();
foreach(var hexagon in hexagons) {
var geometry = hexagon.Key.GetCellBoundary();
var attributes = new AttributesTable();
attributes.Add("count", hexagon.Value);
var f = new Feature(geometry, attributes);
fc.Add(f);
}
var gjw = new GeoJsonWriter {
SerializerSettings = {
NullValueHandling = NullValueHandling.Ignore
}
};
File.WriteAllText("geo.json", gjw.Write(fc));
Console.Write($"Готово. Исходных данных: {someValues.Count}, гексов: {hexagons.Count}");
record SomeData(double Latitude, double Longitude, int Value);
Важно: в целом, в геосистемах используется подход координат в формате longitude/latitude, но в случае Яндекса по умолчанию принимает формат наоборот: latitude/longitude. Формат настраивается на стороне Яндекса, но этот нюанс важно учитывать.
Итак, здесь менее чем в 50 строках кода я сделал пример, который из исходных данных генерирует GeoJSON для карт. В блоке generate мы генерируем некие произвольные данные: произвольные координаты плюс какое-то значение по этим координатам. Например, это может быть адрес дома и количество жителей по этому адресу.
В блоке build мы схлопываем эти данные и группируем их по гексам. Обратите внимание на то, что мы создаем гекс каждый раз, но мы проверяем, может, он у нас уже существует в словаре. Этот этап достаточно быстро отрабатывает — он может быть выполнен в режиме реального времени. Но все же, если у вас разрешение не меняется либо у вас фиксированный набор разрешений и исходные данные меняются редко, то имеет смысл эту операцию провести на уровне хранилища данных.
То есть, в случае PostGIS может быть выполнен запрос в самой базе: у вас есть исходные данные в виде строк, и вы прям в базе можете создать производную таблицу, уже содержащую эти данные. Это позволит вам работать уже со свернутыми данными, что может еще более заметно повысить производительность.
Итак, мы сформировали гексагоны. Теперь их надо отдать как-то на экран. В блоке output мы формируем валидный GeoJSON с нужными нам атрибутами и записываем его в файл.
Давайте попробуем выполнить этот код. У меня заданы определенные координаты (плюс-минус в районе Москвы) и определенное разрешение для гексов. После отработки программы появляется файл geo.json. В моём случае исходных данных было около 3 000, гексов получилось 2 671. Вот таким образом он выглядит:
Это обычный GeoJSON. GeoJSON можно достаточно просто визуализировать. Для этих целей я использую сайт Geojson.io. Если вставить сгенерированный файл на этот сайт, то получится примерно следующая картина:
Если нажать на гекс, высветится информация по содержимому внутри него (например, внутри находится 54 объекта).
В конце хочу показать, что у нас из всего этого получилось в рамках проекта. Так выглядит наша карта:
На этой карте мы отображаем территорию покрытия офисов партнеров Заказчика. Мы можем навести на любое обозначение, и высветится информация, какую площадь занимает данный офис, территорию его покрытия, а также плотность населения на этой территории, количество объектов недвижимости и рекомендуемое количество агентов для работы на этой территории. Работает всё достаточно быстро, плюс мы можем автоматически создать вариант лицензии для партнера (т.е. какую территорию он будет занимать).
Выводы
Изначально эта деятельность стартовала как исследовательская — изучение того, каким образом мы можем помочь партнерам выбрать локацию для открытия офиса. В процессе нескольких итераций и аналитики разработки мы прошли путь от изначально очень общих представлений того, как это решать, до производственного решения и понимания, как использовать Nominatim, библиотеку H3, и как оптимизировать код под нашу задачу.
По итогу, спустя несколько месяцев проект вышел в производство и планируется его использование на постоянной основе для работы с партнерами нашего клиента.
Подведу итоги: использование карт и гексагонов позволит вам быстро реализовать отображение различной информации на карте и использовать эту информацию при принятии решений.
Спасибо за ваше время, потраченное на прочтение статьи.