1. Добавление локальной информации
Для того, чтобы на карту можно было вносить локальную информацию, можно настроить локальный веб-сайт OpenStreetMap и подключаясь к нему с помощью редактора JOSM, редактировать имеющуюся информацию. Я пробовал устанавливать Ruby и Rails и мне даже удалось запустить локальный веб-сайт, однако работал он очень медленно, а ускорить его работу мне не удалось - не хватило знаний Ruby on Rails и времени, чтобы в нём разобраться.
Хотя, этот вариант в любом случае кажется мне не самым лучшим. Ведь в таком варианте настройки нельзя будет отделить локальные данные от данных, загруженных из проекта OpenStreetMap. Затрудняется процедура обновления карт OpenStreetMap - ведь нужно оставить локально внесённые данные, обновив при этом всё остальное.
Поэтому я выбрал другой вариант - локальные данные будут храниться в OSM-файле, который можно редактировать с помощью уже знакомого нам редактора JOSM. Локальной информацией может быть, например, информация с расположением торговых точек в случае магазина, с расположением банкоматов в случае банка, платёжных терминалов, телефонов-автоматов, wifi-точек, зон ответственности развозчиков пиццы и т.п.
Этот OSM-файл сразу после редактирования можно импортировать в отдельную базу данных. А для того, чтобы Mapnik отображал информацию из локальной базы данных, нужно добавить в файл стилей /etc/mapnik-osm-data/osm.xml настройки для подключения к новой базе данных и написать стиль отрисовки объектов из неё.
Документацию по написанию файлов стилей можно найти здесь: Mapnik configuration XML.
Вот пример фрагмента файла osm.xml, в котором задаётся стиль отображения некоего зонального деления территорий, берущегося из базы данных zones:
<Style name="zones"> <Rule> &maxscale_zoom0; &minscale_zoom10; </Rule> <Rule> &maxscale_zoom9; &minscale_zoom11; <Filter>not ([name] = '') and [area] = 'yes'</Filter> <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="1" stroke-linecap="round"/> </Rule> <Rule> &maxscale_zoom12; &minscale_zoom13; <Filter>not ([name] = '') and [area] = 'yes'</Filter> <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="2" stroke-linecap="round"/> <TextSymbolizer size="10" allow-overlap="yes" fill="#000000" fontset-name="book-fonts" opacity="1" placement="interior">[name]</TextSymbolizer> </Rule> <Rule> &maxscale_zoom14; &minscale_zoom19; <Filter>not ([name] = '') and [area] = 'yes'</Filter> <LineSymbolizer stroke="#000000" stroke-opacity="1" stroke-width="4" stroke-linecap="round"/> <TextSymbolizer size="20" allow-overlap="yes" fill="#000000" fontset-name="book-fonts" opacity="1" placement="interior">[name]</TextSymbolizer> </Rule> </Style> <Layer name="zones" status="on" srs="&srs900913;"> <StyleName>zones</StyleName> <Datasource> <Parameter name="table">(select * from planet_osm_polygon) as zones</Parameter> <Parameter name="type">postgis</Parameter> <Parameter name="password">password</Parameter> <!-- <Parameter name="host">localhost</Parameter> --> <Parameter name="user">osm</Parameter> <Parameter name="dbname">zones</Parameter> <Parameter name="estimate_extent">false</Parameter> <Parameter name="extent">-20037508,-19929239,20037508,19929239</Parameter> </Datasource> </Layer>Кстати, этот фрагмент стиля является не самым оптимальным, но он первым пришёл мне в голову, а кроме того, он хорошо иллюстрирует возможности файла стилей.
Его неоптимальность заключается в том, что во-первых, запрос извлекает из таблицы все поля, вне зависимости от того, нужны ли они для отрисовки карты или нет. В моём случае достаточно оставить поля way и name - их вполне достаточно для отрисовки контура участка и его номера.
Второй момент - запрос написан не оптимально, т.к. извлекает из таблицы все строки, а Mapnik будет рисовать только те объекты, которые удовлетворяют настройкам фильтра. Вместо этого можно дополнить запрос условием WHERE name IS NOT NULL AND name <> '' AND area = 'yes', а из описания стиля удалить все фильтры.
Третий момент - это настройка extent, в которой указаны границы всего мира, хотя, наверняка, локальные данные находятся в каких-то предсказуемых границах. Например, локальные данные в моём случае ограничиваются только Республикой Башкортостан, Республикой Татарстан и Оренбургской областью. Можно однажды выполнить следующий запрос:
SELECT ST_Extent(way) FROM (SELECT ST_ConvexHull(ST_Extent(way)) AS way FROM planet_osm_polygon UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way FROM planet_osm_point UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way FROM planet_osm_line UNION SELECT ST_ConvexHull(ST_Extent(way)) AS way FROM planet_osm_roads) AS ways;и прописать в настройки extent возвращённые значения. Это позволит Mapnik'у не выполнять запросы к базе данных, если заведомо известно, что запрос не вернёт результатов для интересующей его области.
Более подробно о рекомендациях по оптимизации файла стилей Mapnik можно прочитать в статье Optimize Rendering with PostGIS.
2. Использование информации из базы данных
Кроме заливки информации в локальную базу данных, я также пользуюсь и основной базой данных Mapnik, непосредственно залезая в её недра с помощью SQL-запросов. Поэтому мне в её таблицах бывают нужны некоторые атрибуты объектов, которые по умолчанию не импортируются в базу данных утилитой osm2pgsql.
Чтобы указать дополнительные поля, нужно отредактировать файл стиля базы данных /usr/share/osm2pgsql/default.style
Например, я добавил в файл стиля базы данных колонки addr:city и addr:street, которые берутся из одноимённых атрибутов объектов из файла OSM:
node,way addr:city text linearnode,way addr:street text linearnode означает, что этот атрибут может быть назначен точке и должен быть импортирован в таблицу planet_osm_point.
way означает, что этот атрибут может быть назначен контуру (линии, дороге или многоугольнику) и должен быть импортирован в таблицу planet_osm_line, planet_osm_roads или planet_osm_polygon.
Теперь информацию из базы данных можно извлекать с помощью SQL-запросов, в чём особенно помогают различные функции PostGIS.
Вот лишь краткий список функций, которые оказались полезными для моих задач:
1. ST_AsText - возвращает геометрический объект в формате WKT (Well-known Text), описанный в стандартах OpenGIS.
2. ST_Transform - переводит координаты опорных точек геометрического объекта из одной проекции в указанную.
3. ST_GeomFromText - возвращает геометрический объект по его описанию в формате WKT и (опционально) заданной проекции.
4. ST_IsValid - проверяет правильность объекта - замкнутость многоугольника, отсутствие самопересечений и т.п.
5. ST_PointOnSurface - возвращает точку, находящуюся строго на поверхности объекта (многоугольника или мультиполигона, многоугольника с дырами - геометрического объекта, имеющего один внешний контур и произвольное количество внутренних контуров).
6. ST_ContainsProperly - функция, возвращающая "истину", если второй объект находится строго внутри первого. Достаточно, чтобы хотя-бы одна вершина второго объекта не попала внутрь первого, или попала в дыру первого объекта, чтобы функция вернула "ложь".
7. ST_Extent - агрегатная функция (работает подобно агрегатным функциям COUNT, MIN, MAX, SUM или AVG), возвращает геометрический объект BOX - прямоугольник, охватывающий выбранные геометрические объекты.
Для чего можно использовать эти функции? Приведу несколько примеров, иллюстрирующих, как их использую я.
Например, для того, чтобы удалить из таблицы planet_osm_polygon многоугольники с самопересечениями и просто многоугольники, имеющие какие-то ошибки, можно воспользоваться таким запросом:
DELETE FROM planet_osm_polygon WHERE NOT ST_IsValid(way);Или можно вернуть координаты точки на поверхности каждого дома из таблицы planet_osm_polygon в формате WKT в проекции WGS 84:
SELECT ST_AsText(ST_Transform(ST_PointOnSurface(way), 4326)) FROM planet_osm_polygon WHERE building IS NOT NULL;Или, например, найти, контур здания по точке внутри него:
SELECT way FROM planet_osm_polygon WHERE building IS NOT NULL AND ST_ContainsProperly(way, ST_Transform(ST_GeomFromText('POINT(48.2445263783448 55.8405766215408)', 4326), 900913));Где 48.2445263783448 - долгота, 55.8405766215408 - широта.
Или вычислить прямоугольник, содержащий весь населённый пункт с указанным именем:
SELECT ST_Extent(ST_Transform(way, 4326)) FROM planet_osm_polygon WHERE place IN ('city', 'town', 'village', 'hamlet') AND name = 'Салават';Естественно, чтобы извлекать значения полей addr:city, addr:street, нужно их сначала добавить в файл стиля базы данных для утилиты osm2pgsql, а затем импортировать данные, что мы уже проделали в предыдущем пункте этой заметки. Правда, не всегда и везде проставляются значения этих полей, потому что для отрисовки карты Mapnik их никак не использует - поверх дома выводится только его номер.
Но некоторые поля можно проставить довольно просто. Например, чтобы проставить поле "addr:city" у всех домов, попадающих в административную границу какого-либо населённого пункта, я пользуюсь скриптом на Perl, часть которого приведена ниже:
# Перебираем населённые пункты, прописываем домам населённый пункт в поле addr:city sub osm_fill_city() { my $total = 0; my $sth_polygon = $dbh_o->prepare("UPDATE planet_osm_polygon SET \"addr:city\" = ? WHERE building IS NOT NULL AND (\"addr:city\" IS NULL OR \"addr:city\" = '') AND ST_ContainsProperly(ST_GeomFromText(?, 900913), way)"); my $sth_point = $dbh_o->prepare("UPDATE planet_osm_point SET \"addr:city\" = ? WHERE building IS NOT NULL AND (\"addr:city\" IS NULL OR \"addr:city\" = '') AND ST_ContainsProperly(ST_GeomFromText(?, 900913), way)"); my $sth_city = $dbh_o->prepare("SELECT name, ST_AsText(way) FROM planet_osm_polygon WHERE place IN ('city', 'town', 'village', 'hamlet') AND name IS NOT NULL AND name <> ''"); $sth_city->execute(); while (my ($name, $wkt) = $sth_city->fetchrow_array()) { $sth_polygon->execute($name, $wkt); $sth_point->execute($name, $wkt); $total++; print "Простановка населённых пунктов на зданиях, всего обработано населённых пунктов: $total\r"; } $sth_city->finish(); $sth_point->finish(); $sth_polygon->finish(); print "Простановка населённых пунктов на зданиях, всего обработано населённых пунктов: $total\n"; }Можно, конечно, не заниматься этим, а взять координаты или контур интересующего нас объекта и с помощью функции ST_ContainsProperly узнать, в административные границы какого населённого пункта этот объект попадает.
3. Геокодинг - поиск географических объектов
Эту информацию без дополнительной обработки можно использовать для обратного геокодинга, то есть для получения адреса здания по географическим координатам точки, попавшей в контур здания:
SELECT "addr:city", "addr:street", "addr:housenumber" FROM planet_osm_polygon WHERE building IS NOT NULL AND building <> '' AND "addr:city" IS NOT NULL AND "addr:city" <> '' AND "addr:street" IS NOT NULL AND "addr:street" <> '' AND "addr:housenumber" IS NOT NULL AND "addr:housenumber" <> '' AND ST_ContainsProperly(way, ST_Transform(ST_GeomFromText('POINT(55.98886 54.74241)', 4326), 900913));Прямой же геокодинг - нахождение координат дома по адресу - не является столь же тривиальной задачей, как обратный геокодинг. Это так, потому что людей довольно трудно заставить писать адрес всегда одним и тем же образом. Люди используют сокращения слов, переставляют слова местами, пропускают слова, кажущиеся им незначимыми, а подобные знания в "голову" компьютера не заложишь.
Для себя я нашёл подходящее решение, которое в целом меня устраивает, но не является универсальным, т.к. опирается на некоторые допущения, которые верны для интересующих меня населённых пунктов.
Для поиска адреса создаётся индекс адресов, в который помещаются "нормализованные" строки, содержащие название населённого пункта, улицы и дома. Перед поиском адреса по индексу, искомый адрес тоже переводится в нормализованную форму, а дальнейший поиск выполняется простым SQL-запросом.
Процедура нормализации у меня делится на три части, из которых самой сложной является нормализация названия улицы.
Нормализация названия населённого пункта:
- Буквы переводятся в нижний регистр,
- Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
- Буква "ё" заменяется на "е",
- Удаляются сокращения "г.", "п.", "с.", "д.", слова "город", "поселок", "село", "деревня".
Из неучтённых особенностей тут могут быть одноимённые населённые пункты разного класса. Например, посёлок Октябрьский и город Октябрьский. Или одноимённые населённые пункты из разных районов - посёлок Фёдоровка рядом с Уфой и посёлок Фёдоровка в Фёдоровском районе. Но поскольку мне нужен поиск адресов только в 9 городах, то эти особенности я учитывать не стал.
Нормализация номера дома:
- Буквы переводятся в нижний регистр,
- Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
- Буква "ё" заменяется на "е".
Нормализация названия улицы:
- Буквы переводятся в нижний регистр,
- Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
- Буква "ё" заменяется на "е",
- Получившаяся строка разбивается на последовательность слов, а границами слов считаются пробелы и точки. Это сделано для того, чтобы различные сокращения и инициалы отделились от слов, с которыми они написаны слитно,
- Удаляются одиночные буквы,
- Раскрываются сокращения "ул" -> "улица", "пер" -> "переулок", "пр" -> "проспект", "пер" -> "переулок", "бул" -> "бульвар", "пл" -> "площадь", "шос" -> "шоссе", "наб" -> "набережная", "им" -> "имени",
- От чисел отрезаются окончания, так что строки типа "60-летия", "2-й", "1-я" превращаются просто в числа,
- Удаляются незначащие слова типа "лет", "летия", "реки", "имени". Названия многих улиц приурочены к юбилеям каких-либо памятных событий ("50-летия Октября" или "60 лет СССР"). Набережные, естественно, часто имеют в своём названии названия рек, вдоль которых они расположены, поэтому между названием типа "набережная реки Уфы" или "набережная Уфы" нет никакой разницы. И, наконец, улицы часто называются в честь каких-то людей, поэтому нет разницы между названиями типа "проспект имени Ленина" или "проспект Ленина",
- Удаляются слова-классификаторы адреса типа "улица", "проспект", "площадь", "тракт", из которых запоминается только первое.
- Из оставшихся слов собирается нормализованный адрес, перед которым ставится слово-классификатор адреса.
- В получившейся строке ищутся идущие подряд пары слов типа "имя фамилия" или "титул фамилия", из которых остаётся только фамилия. Тут я делаю предположение, что в городе не бывает улиц одного класса, названных именами однофамильцев. То есть, в городе не может быть улицы Льва Толстого и улицы Алексея Толстого, но может быть улица Льва Толстого и проспект Алексея Толстого - в этом случае однофамильцы будут различаться классом улицы. И сюда же относятся различия в титулах - алгоритм нормализации не учитывает, что могут быть улицы академика Морозова и Павлика Морозова. Это преобразование помогает находить названия улиц, в случае если имя или титул человека, в честь которого названа искомая улица, не были указаны. Тут мне пришлось приложить усилия и составить список людей, именами которых названы улицы. У меня это единый список, но вообще, хорошо бы иметь отдельный список для каждого населённого пункта - так и точность и скорость нормализации будут выше. В России для этого можно использовать адресный справочник КЛАДР или пришедший ему на смену ФИАС - читайте, например КЛАДР умер, да здравствует ФИАС?
1 комментарий:
Сразу хочу сказать спасибо за ваши статьи, они мне очень помогли. Локальный сервер карт я установил, теперь они отображаются нормально. У меня тут вопросы возникли:
1 Вы не пробовали данные переводить в формат Nominatime, чтобы данные вытаскивать в виде:
4, улица Новый Быт, Якиманиха, Kostroma, Kostroma Oblast, Central Federal District, 156007, Russian Federation;
2 Организовать построение маршрутов с помощью gosmore.
Пытаюсь сделать все эти вещь, но пока одни только ошибки. Gosmore установил через apt-get install gosmore, так как исходники так нормально и не скомпилировались на Ubuntu 12.04(
Может поделитесь мыслями как это все сделать. У Вас опыта гораздо больше! Спасибо.
Отправить комментарий