Эта статья, как и приложение были написаны на прошлых новогодних праздниках, но по ряду причин, я не решился публиковать текст, так и остался он в черновиках. Вчера случилось страшное - в канун новогодних праздников город Белгород, в котором я живу, попал под массированный обстрел кассетными боеприпасами, моя семья также попала в сектор бомбардировки, чудом спаслись и не пострадали. Все произошло быстро, эти несколько минут перевернули жизни сотен семей. Когда мы вышли из укрытия, стало понятно, что будут десятки погибших. Увы, не сработали никакие системы превентивного предупреждения.
Разработка Telegram-бота для поиска ближайших укрытий. В статье описан опыт разработки средства систематизации и поиска укрытий. Разработка была выполнена на добровольных началах в ответ на запрос населения о расположении укрытий.
Также в материале рассматривается способ расчета расстояний между точками, заданными географическими координатами, на первый взгляд эта задача может показаться тривиальной, но это не совсем так. Подумайте, как это можно сделать пока не прочитали статью.
Введение
Уверен, многие знают о том, что у жителей регионов, находящихся в непосредственной близости с проведением СВО появилась не шуточная, а вполне реальная угроза попасть под обстрел, в ответ на угрозу власти приграничных регионов приняли ряд организационных мер: появились «интерактивные карты укрытий», на улицах городов установили информационные таблички-указатели, позволяющие оперативно сориентироваться и найти ближайшее укрытие.
Какие меры предпринимаются для информирования населения? Ответственное за данное мероприятие Министерство Чрезвычайных Ситуаций рассылает сообщения о начале обстрела и его завершении. Здорово? Нет! Не очень, так, мне и моим близким сообщения о начале обстрела приходят с задержкой от часа до двух. Я не знаю в чем причина такой «оперативности», но догадываюсь, что она находится где-то в диапазоне от необходимости пройти ряд согласований ответственных руководителей перед отправкой – до технологических ограничений оборудования операторов связи.
Также имеется система громкоговорителей, её даже иногда тестируют, пугая сиренами горожан, не знающих о том, что тревога учебная, но, к сожалению, в реальной работе она замечена пока не была.
И вот какая занятная ситуация получается, формально для информирования у нас все есть – и укрытия, и сирены, и интерактивные карты, и СМС-информирование, но в реальных условиях весь этот набор показал свою полную бесполезность. Закрытые укрытия, неработающие сирены, позднее реагирование, сообщения приходящие через полтора часа после завершения обстрела – это объективная реальность. Очевидно, что ответственные за организацию данных мероприятий попросту не были готовы к подобным вызовам. А кто был готов?
Для IT-сообщества организация и оптимизация бизнес-процессов – это повседневная рутина и благодаря накопленному нами опыту, в наших силах помочь улучшить процессы, направленные на обеспечение безопасности гражданского населения. Как говорится, от каждого по возможности. Я бы назвал безвозмездную помощь со стороны разработчиков – IT-волонтерством.
Когда я получил СМС-оповещение об обстреле через несколько часов после его завершения, стало очевидно, что СМС-рассылка – это не про скорость и данный канал явно не годится для оперативного информирования. Так пришла идея написать Telegram-бот, который при получении сообщения от подтвержденных аккаунтов ответственных лиц, будет публиковать информацию в новостные паблики, к которым подключен. Увы, представители МЧС пока не отреагировали на данную инициативу, а без их участия получить подтвержденную информацию не получится.
Окопы идей гораздо лучше, чем окопы из камней
Хосе Марти (1853–1895) — кубинский поэт
К «интерактивной картой укрытий» тоже есть вопросы. Оцените "удобство" использования – для начала придется перейти по ссылке вида https://yandex.ru/maps/4/belgorod/?ll=36.675005%2C50.584049&mode=usermaps&source=constructorLink&um=constructor%3A235fb866f1f5245c11290cb8e1e22527dadcee2acc5414a4cc6dcd30d732de68&z=16 , очевидно, что её невозможно запомнить, поэтому первым делом придется достать её из закладок браузера, затем спозиционироваться на карте и среди нескольких сотен объектов выбрать ближайший, карта может начать тормозить, она хоть и «динамическая», но не умеет фильтровать данные и предлагать оптимальные точки на основе информации о вашем местоположении, кроме того, в каждом населенном пункте своя «интерактивная карта». Ну как?
От идеи - к реализации
Ввиду того, что без участия ответственных лиц наладить информирование не получится, пришло решение изменить первоначальнозадуманный функционал, а именно, сделать приложение, которое по отправленной боту геопозиции, определит ближайшие укрытие, а также будет информировать пользователя о том, как вести себя в случае обстрела.
Для реализации этой задачи я использовал следующий стек:
-
Telegram API – для разработки бота;
-
фреймворк Spring – на его основе построен бэкэнд сервиса;
-
библиотека Playwright – для парсинга карт;
-
компонент Spring Shell – для организации интерфейса командной строки управления бэкэндом;
-
СУБД PostgreSQL – для хранения информации об укрытиях;
-
OSM API и 2Gis API – для геокодирования объектов.
Первая проблема, с которой я столкнулся – это отсутствие списков укрытий на сайтах ответственных ведомств. Найти удалось лишь ссылки на «интерактивные карты», но даже список ссылок этих карт отсутствует. Может у кого-то из читателей получится отыскать систематизированный список укрытий или «интерактивных карт», мне сделать этого не удалось.
Итак, у нас имеется созданная пользователем Яндекс-карта, которая содержит список точек, в описании точек присутствует адрес и дополнительная информация об укрытии. Но как сделать эту информацию доступной для обработки? Для этого я использовал библиотеку Playwright от Microsoft. Информацию о координатах точки получить с карты в явном виде не получится, здесь будет уместно использовать сервисы геокодирования.
Геокодирование
Геокодирование - это процесс назначения географических идентификаторов (широта, долгота) объектам карты. Есть множество сервисов геокодирования как платных, так и бесплатных. Для решения задачи получения координат по адресу можно воспользоваться сервисами DaData, 2Gis, OSM, Yandex Map и им подобными. Изначально я воспользовался "народным" сервисом OSM, и он отлично показал себя при геокодировании объектов г. Белгород. Из 859 укрытий, отмеченных на карте, удалось определить координаты 850 объектов, при этом 8 объектов не имели адреса, т.к. оказались подземными переходами, а адрес единственного ошибочного объекта был записан некорректно изначально. Великолепный результат! Жаль, что в более мелких населенных пунктах точность определения была куда ниже, т.к. многие строения не внесены в базу "народного" сервиса. Придется пользоваться платными сервисами.
Никогда и ничего не просите! Никогда и ничего, и в особенности у тех, кто сильнее вас.
М. А. Булгаков — русский писатель советского периода
1 января я сделал запрос на получение ключей для реализации некоммерческого проекта в компанию 2ГИС для этого необходимо было описать задачу и оставить контактные данные.
Описание задачи
Хочу разработать телеграм-бот для поиска населением укрытий от бомебежек в г. Белгороде и области. Для решения данной задачи мне необходимо определить координаты объектов по адресу, т.к. список объектов имеется только в виде списка адресов.
10 января мне предоставили ключ, сроком на 3 месяца и лимитом в 5000 запросов. Большое спасибо! Сервис отлично себя зарекомендовал, точность определения объектов просто феноменальная, 2ГИС знают свое дело. Благодарю.
Но есть одно но, о котором я не могу не написать, 30 января вышла новость "Интерактивная карта укрытий Белгорода доступна на базе приложения 2ГИС т.е. компания через несколько недель после моего запроса с описанием идеи по геокодированию укрытий г. Белгорода выполнила, геокодирование укрытий г. Белгорода. Наверное, это просто совпадение, ведь не может такая крупная и уважаемая компания пользоваться идеями своих пользователей, которые предварительно просят подробно описать в обмен на бесплатный пробный период. Или может? Камрады, что думаете?
Как бы то ни было, мы работаем над решением социально-острой актуальной проблемы – это обеспечение безопасности мирного населения. На карте 2ГИС отсутствуют укрытия в подземных переходах, полагаю, что при загрузке разработчики столкнулись с той же проблемой, что и я – это отсутствие адресов у подземных переходов и не обработали эти объекты вручную. Было бы здорово, если компания 2ГИС загрузит все имеющиеся карты, список официальных карт укрытий у меня в приложении. И будет совсем уж круто, если точками на карте будут отмечены входы в укрытия, а не дома, где они расположены, мне одному эту задачу точно не потянуть.
Геокодирование 2Gis API
Для получения данных необходимо отправить запрос вида:
https://catalog.api.2gis.com/3.0/items/geocode?q=%a&fields=items.point&key=%k
, где:
%a – адрес объекта;
%k - ключ.
Например:
https://catalog.api.2gis.com/3.0/items/geocode?q=Белгород+Белгородский+проспект+54&fields=items.point&key=1234567
Ответом на запрос из примера выше будет:
Пример ответа
{
"meta": {
"api_version": "3.0.16667",
"code": 200,
"issue_date": "20230130"
},
"result": {
"items": [
{
"address_name": "Белгородский проспект, 54",
"full_name": "Белгород, Белгородский проспект, 54",
"id": "6474560119524249",
"name": "Белгородский проспект, 54",
"point": {
"lat": 50.602148,
"lon": 36.595258
},
"purpose_name": "Жилой дом",
"type": "building"
}
],
"total": 1
}
}
Ответ содержит все необходимые мне данные.
Геокодирование OSM (Open Street Map) API
Для получения данных необходимо отправить запрос вида:
https://nominatim.openstreetmap.org/search?q=%a&format=json&addressdetails=1
, где %a – адрес объекта.
Например:
https://nominatim.openstreetmap.org/search?q=Белгород+Белгородский+проспект+54&format=json&addressdetails=1
Ответом на запрос из примера выше будет:
Пример ответа
[
{
"place_id": 74704640,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "node",
"osm_id": 7125732805,
"boundingbox": [
"50.601968",
"50.602068",
"36.5952937",
"36.5953937"
],
"lat": "50.602018",
"lon": "36.5953437",
"display_name": "Сбербанк, 54, Белгородский проспект, Савино, Восточный округ, Белгород, городской округ Белгород, Белгородская область, Центральный федеральный округ, 308031, Россия",
"class": "amenity",
"type": "bank",
"importance": 0.4200099999999999,
"icon": "https://nominatim.openstreetmap.org/ui/mapicons/money_bank2.p.20.png",
"address": {
"amenity": "Сбербанк",
"house_number": "54",
"road": "Белгородский проспект",
"suburb": "Савино",
"city_district": "Восточный округ",
"city": "Белгород",
"county": "городской округ Белгород",
"state": "Белгородская область",
"ISO3166-2-lvl4": "RU-BEL",
"region": "Центральный федеральный округ",
"postcode": "308031",
"country": "Россия",
"country_code": "ru"
}
},
{
"place_id": 216710583,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "way",
"osm_id": 490130083,
"boundingbox": [
"50.6018962",
"50.6023612",
"36.5951068",
"36.5963762"
],
"lat": "50.602134449999994",
"lon": "36.59534627944597",
"display_name": "54, Белгородский проспект, Савино, Восточный округ, Белгород, городской округ Белгород, Белгородская область, Центральный федеральный округ, 308031, Россия",
"class": "building",
"type": "apartments",
"importance": 0.4200099999999999,
"address": {
"house_number": "54",
"road": "Белгородский проспект",
"suburb": "Савино",
"city_district": "Восточный округ",
"city": "Белгород",
"county": "городской округ Белгород",
"state": "Белгородская область",
"ISO3166-2-lvl4": "RU-BEL",
"region": "Центральный федеральный округ",
"postcode": "308031",
"country": "Россия",
"country_code": "ru"
}
},
{
"place_id": 98520744,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "node",
"osm_id": 9801697670,
"boundingbox": [
"50.6019151",
"50.6020151",
"36.5958033",
"36.5959033"
],
"lat": "50.6019651",
"lon": "36.5958533",
"display_name": "Магазин Горящих Путёвок, 54, Белгородский проспект, Савино, Восточный округ, Белгород, городской округ Белгород, Белгородская область, Центральный федеральный округ, 308031, Россия",
"class": "shop",
"type": "travel_agency",
"importance": 0.4200099999999999,
"address": {
"shop": "Магазин Горящих Путёвок",
"house_number": "54",
"road": "Белгородский проспект",
"suburb": "Савино",
"city_district": "Восточный округ",
"city": "Белгород",
"county": "городской округ Белгород",
"state": "Белгородская область",
"ISO3166-2-lvl4": "RU-BEL",
"region": "Центральный федеральный округ",
"postcode": "308031",
"country": "Россия",
"country_code": "ru"
}
}
]
Как видите, ответ содержит исчерпывающую информацию.
С укрытиями, координаты которых не удалось определить с помощью геокодеров OSM и 2Gis, пришлось разбираться в ручном режиме, в этом мне помог сервис Яндекс-карт для определения координат. Кроме того, я поправил ошибки в описании некоторых объектов и постарался привести написание адресов к единому формату. В результате у меня появилась база данных с координатами, адресами и описанием укрытий. Осталась, казалось бы, простая задача – определить ближайшие к переданным пользователем координатам укрытия. Но на деле оказалось не все так просто.
Дрейфую в океане мыслей и информации в поиске своих собственных координат.
Евгений Ханкин
Работа с координатами
Географические (сферические) координаты определяют положение точки на земной поверхности, задаются двумя углами:
-
широта – угол φ между плоскостью экватора и точкой (-90° – 90°), рисуется на картах параллелями;
-
долгота – угол λ между нулевым меридианом и точкой (-180° – 180°), рисуется на картах линиями от полюса до полюса.
Нулевой меридиан – географический меридиан, используемый как начало отсчёта географической долготы, международным соглашением принято считать меридиан, проходящий через пригород Лондона – Гринвич.
Координаты могут записываться множеством способов, но на картах OSM, Google и Яндекс используется формат: широта, долгота в градусах с десятичной дробью, такой формат буду использовать и я. Так, например, записываются координаты дома, где находится мой офис: 50.6020,36.5959.
С географическими координатами все понятно, но как определить расстояние между двумя точками, заданными широтой и долготой? Вся соль заключается в том, что географические координаты, как я упоминал, задаются двумя углами, а расстояния вычисляются в, привычной нам, прямоугольной (Декартовой) системе координат.
Расстояние между пунктами A и B можно вычислить следующим образом:
Согласно законам сферической тригонометрии расстояние d (в радианах) между пунктами A и B вычисляется по формуле:
Радиан – это угол, соответствующий длине дуге, длина которой равна её радиусу.
– средний радиус Земли
Формулы для вычисления смещения по долготе и широте:
– угол в радианах, выражающий смещение по долготе на расстояние dX;
– угол в радианах, выражающий смещение по широте на расстояние dY;
– приведение угла в радианах к углу в градусах.
Ну и кто говорил, что не пригодится эта ваша тригонометрия?
Для работы с расстояниями я реализовал несколько простых классов:
GeoTools – инструменты для работы с географическими координатами
import static java.lang.Math.*;
public class GeoTools {
public static final double EARTH_R = 6378137;
public static double getDistance(double latA, double lonA, double latB, double lonB) {
double radLatA = Math.toRadians(latA);
double radLatB = Math.toRadians(latB);
double radLonA = Math.toRadians(lonA);
double radLonB = Math.toRadians(lonB);
return Math.acos(sin(radLatA) * sin(radLatB) + cos(radLatA) * cos(radLatB) * cos(radLonB - radLonA)) * EARTH_R;
}
public static double getDistance(Point pointA, Point pointB) {
return getDistance(pointA.getLat(), pointA.getLon(), pointB.getLat(), pointB.getLon());
}
public static double latOffsetRad(double dX) {
return dX / EARTH_R;
}
public static double lonOffsetToRad(double dY, double lat) {
return dY / (EARTH_R * cos(lat));
}
public static double latOffsetToDeg(double dX) {
return Math.toDegrees(latOffsetRad(dX));
}
public static double lonOffsetToDeg(double dY, double lat) {
return Math.toDegrees(lonOffsetToRad(dY, Math.toRadians(lat)));
}
public static Point pointOffset(Point point, double dX, double dY) {
double lat = point.getLat() + latOffsetToDeg(dX);
return new Point(lat, point.getLon() + lonOffsetToDeg(dY, lat));
}
}
Point – класс для хранения географических координат точки
public class Point {
private final Double lat;
private final Double lon;
public Point (Double lat, Double lon) {
this.lat = lat;
this.lon = lon;
}
public Double getLat() {
return lat;
}
public Double getLon() {
return lon;
}
}
Чтобы найти ближайшие к пользователю убежища, я рассчитывал координаты вершин квадрата с заданной длиной грани, затем с помощью SQL-запроса к базе данных выбирал точки, попадающие в заданный диапазон, если ни одно убежище не было найдено, то увеличивал длину граней. Полученные результаты, отсортированные по удаленности, отправляются в ответном сообщении. Следом отправляется Геопозиция ближайшего укрытия.
Интерфейс командной строки Spring Shell
Для администрирования сервиса был разработан интерфейс командной строки (CLI), который позволяет мне парсить карты, добавлять и изменять существующие объекты с помощью команд. Данный инструмент позволяет мне достаточно оперативно обновлять списки укрытий, а также тестировать разработанный функционал.
Демонстрация работы интерфейса командной строки Spring Shell
Пример работы с командной строкой. В данном случае выполняется парсинг карты города Старый Оскол. Как видно из лога, из 333 укрытий были определены координаты 330, объекты для которых не определились координаты - это подземные переходы, они не имеют адреса, такие точки обрабатывались вручную.
Telegram-бот в качестве интерфейса пользователя
А что с фронтэндом? Взаимодействовать с пользователем будем через Telegram-бот. Для многих такое решение может показаться непривычным, но это только поначалу. Ведь если задуматься, инструменты, предоставляемые командой Павла Дурова имеют ряд преимуществ перед классическими приложениями. Никто, я надеюсь, не слышал про заблокированные по причине санкций боты? Я бы на месте организаций, попавших под санкции, обратил внимание на этот вид приложений, ведь они имеют ряд приемуществ. Например, удобство установки, немногие вспомнят, когда в последний раз устанавливали новое приложение на свой мобильник, некоторые и вовсе забыли пароли от своих аккаунтов, а сложность регистрации в некоторых приложениях может вывести из себя. В отличие от классических мобильных приложений, Telegram всегда под рукой и готов к использованию. Нелишним будет вспомнить и про безопасность, инфраструктура Telegram является своеобразным прокси, злоумышленнику будет сложно узнать сервер, на котором запущен бот, со всеми вытекающими. А Вы что думаете по поводу замены классических приложений Telegram-ботами?
В итоге у меня получился бот @ShelterBaseBot, Вы можете и сами протестировать его работу, и пусть он вам никогда не пригодится!
Новость о моем боте быстро облетела наши региональные СМИ, люди не только благодарили, но и давали советы по улучшению. В первые дни были обработаны тысячи запросов, значит мои труды не напрасны.
Заключение
Полный код выкладывать не стал, ибо получится длиннопост, но, если кому-то будет интересно как реализованы те или иные компоненты, пишите в комментариях, обсудим.
В планах добавить возможность ответственным за безопасность официальным лицам посылать сообщение о начале и окончании обстрела, бот по последней отправленной геопозиции будет информировать пользователей заданного района. Как мы все понимаем, эту доработку невозможно сделать без непосредственного участия сотрудников МЧС.
Есть несколько предложений и советов официальным лицам, ответственным за безопасность и информирование населения:
-
исправить ошибки на картах, очистить наименования от нежелательных символов;
-
присвоить уникальные идентификаторы укрытиям;
-
выложить список укрытий на официальных сайтах ответственных ведомств, в списках должны присутствовать не только адреса объектов, но и их координаты, т.к. у многих объектов отсутствуют адреса;
-
региональным властям перейти к единому формату хранения и отображения карт укрытий.
Приложение
Карты укрытий:
Сервисы для геокодирования:
Постскриптум
Включил новостные телеканалы и у меня создалось впечатление, что у нас 2 России - в одной десятки погибших, сотни покалеченных и тысячи исковерканных жизней, а в другой - праздник, КВН и новогодний огонек. Я живу в первой, большинство из тех, кто дочитал этот текст - во второй. Извините, что испортил вам настроение, ушел в пункт волонтерской помощи, комментарии почитаю потом.
Скорбим...
Спасибо всем за внимание! Вопросы пишите в личку и Telegram, @SkyZion.
UPD: комментарии отключили админы, видимо, кто-то не удержался и ушел от темы IT - к теме политики.
Автор: Andrey