Однажды после переезда пришлось озадачиться поиском своего почтового отделения. В последних версиях настольной версии 2Гис, к счастью, для большинства городов имеется информация об индексах зданий и в конечном итоге поиск свелся к выбору почтового отделения по номеру, равному последним трем цифрам индекса, однако число рутинных операций для этого было достаточно велико и захотелось на досуге в качестве разминки для ума и из любви к прикладным алгоритмам попытаться этот процесс автоматизировать.
Попытка №1
Сначала было решено пойти «в лоб», то есть найти способ получить по индексу сразу адрес почтового отделения. Способ нашелся достаточно быстро — на сайте почты России имеется сервис Поиск отделений почтовой связи, реализующий данную задачу. Хотя стабильность и скорость работы данного сервиса оставляет желать лучшего (последние несколько дней он вообще отключен в связи с проведением профилактических работ), было найдено достаточно сторонних сервисов, зеркалирующих эту информацию, например ГдеПосылка или Независимый рейтинг почтовых отделений России. Однако возникла другая проблема — полученный адрес можно было только целиком скормить приложению через пользовательский интерфейс в универсальное поле «Где»:
Однако API плагинов такой возможности не имеет, методу поиска нужно предать три отдельных поля: населенный пункт, улица, дом. В принципе, на этом этапе можно было бы попробовать озадачится выкусыванием из полученной строки адреса необходимых полей, но эта задача не представлялась интересной и было решено пойти в обход (возможно сейчас я бы вернулся к этому варианту).
Попытка №2
Кроме адреса почтового отделения, приведенные выше сервисы так же сообщают его название, которое, в общем случае, состоит из наименования населенного пункта и порядкового номера отделения если их несколько. Этой информации, в общем случае, должно хватить для поиска по справочнику организаций (по населенному пункту и наименованию отделения). На данном этапе был реализован первый рабочий вариант плагина, опрашивающий все три приведенных сервиса исходя из их доступности (репозиторий на GitHub). Однако в любом случае время отклика было нестабильным да и идеология настольного приложения 2Гис предполагает возможность полноценной работы в offline-режиме.
Попытка №3
После непродолжительных поисков offline-базы был найден Эталонный Справочник Почтовых индексов объектов почтовой связи, представляющий собой DBF файл, да еще и обновляющийся с завидной периодичностью. Из 18-мегабайтного файла была сделана выборка только необходимой информации и включена в плагин.
Однако на данном этапе был обнаружен ряд проблем с именами почтовых отделений и их отнесению к населенным пунктам. Практически все из них удалось решить в текущей реализации плагина (текущая ветка на GitHub).
Ниже представлен основной алгоритм формирования критериев для поиска почтового отделения в справочнике организаций:
// Получаем название почтового отделения
string postOfficeName = LocalFileInformationService.Instance.GetPostOffice(postIndex);
if (postOfficeName != null)
{
// Название отделения в виде "<Населенный пункт> <номер>"
Match m = CITY_POST_OFFICE_NAME.Match(postOfficeName);
String city;
String number;
if (m.Success)
{
// Почтовое отделение с номером
city = m.Groups[1].Value;
number = m.Groups[2].Value;
}
else
{
// Почтамт, либо единствненое отделение в маленьком населенном пункте
city = postOfficeName;
number = null;
}
try
{
ICriteriaSet criteries = _pBaseView.Factory.CreateCriteriaSet();
// ищем организации в рубрике "Почтовые отделения"
criteries.set_Criterion("grym_rub:name", "Почтовые отделения");
string gisCityName;
if (_cities.TryGetValue(city, out gisCityName))
{
// если в базе есть населенный пункт с названием, совпадающим с названием почтовго отделения, локализуем поиск в данном населенном пункте
criteries.set_Criterion("grym_city:name", gisCityName);
}
if (number != null)
{
// если у отделения есть номер, скорее всего он будет в его названии
criteries.set_Criterion("grym_name", number);
}
else
{
// узнаем число отделений в населенном пункте отделения
int officesCount = LocalFileInformationService.Instance.GetCityPostOffices(city, postIndex.Substring(0, 3));
if (officesCount > 2)
{
// если в городе больше двух (для верности) почтовых отделений, то отделение без номера скорее всего называется "Почтамт"
criteries.set_Criterion("grym_name", "Почтамт");
}
else if (String.IsNullOrEmpty(gisCityName))
{
// иначе если не удалось локализовать поиск по населенному пункту (например пос. Светлый в г. Томск не входит в базу населенных пунктов), ищем назвнаие населенного пункта в названии почтового отделения
// остается вопрос как быть с почтовыми отделениями, названия которых не соответствуют названиям населенных пунктов, например отделенеие Томь в Черной речке и Тимирязевский в Тимирязево.
if (((int)dr.Value["addr_count"]) > 0)
{
// определяем город в котором находится данный дом
string featureCity = dr.Value["city"].ToString();
// узнаем число отделений в населенном пункте к которому относится здание
int officesCount2 = LocalFileInformationService.Instance.GetCityPostOffices(NormalizeCityName(featureCity), postIndex.Substring(0, 3));
if (officesCount2 > 0)
{
// если мы находимся в населенном пункте с несколькими отделениями, значит скорее всего мы в поселке, входящем в состав города (не вынесен как отдельный населенный пункт) (пос. Светлый, Томск)
// значит нужно искать по названию отделения
criteries.set_Criterion("grym_name", city);
}
else
{
// в населенном пункте нет почтовых отделений называющихся так же как и сам населенный пункт. Странно, придется просто вывести все почтовые отделения в населенном пункте
// например почтовое отделение в пос. Черная речка, Томск называется Томь
criteries.set_Criterion("grym_city:name", featureCity);
}
// else Как быть с селом Тимирязево, который входит состав города Томска, а почтовое отделение называется Тимирязевский?
}
else
{
// Дом без адреса? Странно, откуда тогда у него индекс
criteries.set_Criterion("grym_name", city);
}
}
}
_pBaseView.Frame.DirectoryCollection.Search(criteries, "Почтовое отделение " + postIndex, "<criterion>Почтовое отделение</criterion><description>" + postOfficeName + "</description>");
}
catch (Exception e)
{
MessageBox.Show(e.Message + e.StackTrace + e.GetType().ToString());
}
}
else
{
MessageBox.Show("Упс, похоже такого индекса не существует.");
}
Осталась только проблема с селом Тимирязево, официально входящим в состав города Томска — отделение в нем называется «Тимирязевский», но само село не выделено в справочнике как самостоятельный населенный пункт (что так и есть). Возможно стоит вернуться к пункту 1 и попытаться поработать с адресной информацией.
Попытка №4
Еще было предположение, что здание, в котором располагается почтовое отделение имеет тот же индекс, что и само отделение, но это оказалось не так, например Почтовое отделение №39 в Прокопьевске имеет индекс 653033. К тому же API 2Гис не позволяет в критериях поиска организаций указывать почтовый индекс здания.
Плюшки и улучшения.
Автообновление.
Учитывая частоту выхода обновлений эталонного справочника, есть желание дописать функцию автообновления базы индексов.
Интерфейс.
Когда задумывался плагин, было желание максимально встроить его функциональность в имеющийся интерфейс, т.е сделать индекс здания в информационной карточке здания гиперссылкой, инициирующей поиск:
Однако штатное API не предусматривает такой возможности, но был найден обходной путь с подменой контроллера данной вкладки (реализован в первой версии плагина):
class CustomMainController : IMapInfoController, IControlAppearance, IObjectCustomization
{
private IMapInfoController _innerController;
private IBaseViewThread _pBaseView;
private string _currentCity;
public CustomMainController(IBaseViewThread pBaseView)
{
_innerController = ((GrymCore.IMapInfoControllers2)pBaseView.Frame.Map.MapInfoControllers).FindMapInfoController("Grym.MapInfo.Default");
_pBaseView = pBaseView;
_currentCity=_pBaseView.BaseReference.Name;
((GrymCore.IMapInfoControllers2)pBaseView.Frame.Map.MapInfoControllers).RemoveController(_innerController);
((GrymCore.IMapInfoControllers2)pBaseView.Frame.Map.MapInfoControllers).AddController(this);
PostalInformationServiceManager.Instance.BaseViewThread = _pBaseView;
}
public bool Check(IFeature f)
{
return _innerController.Check(f);
}
...
}
Однако но данная реализация имеет некоторые проблемы: вкладка информация перемещается в конец списка и появляются сбои при одновременном запуске нескольких экземпляров справочника для разных городов), поэтому пока от идеи пришлось отказаться в пользу штатного механизма кастомайзеров, позволяющего добавлять пользовательские ссылки в подвал карточки.
Заключение
На данном этапе первичная задач написания плагина (порешать интересную задачку, изучить что-то новое, вспомнить c#) вроде бы выполнена. Кроме того появилась идея другого плагина, расширяющего функциональность фильтра «Работает сейчас». Поэтому надо принимать решение, что делать дальше. В этом хотелось бы прислушаться к мнению сообщества — вдруг кому-то будет интересна данная разработка или появятся интересные идеи для ее развития.
Скачать:
Обычная версия плагина
Версия плагина заменяющая индекс здания на ссылку (осторожно, версия нестабильна и может приводить к падению оболочки 2Гис).
Автор: Askell