В прошлых заметках Подготовка карт для генератора тайлов Mapnik, Настройка базы данных для генератора тайлов Mapnik, Установка генератора тайлов Mapnik и Установка renderd и mod_tile - системы отрисовки тайлов по запросу мы настроили обычный тайловый сервер. Однако, от одного лишь просмотра карт толку мало - ведь то же самое без лишних усилий можно увидеть и на официальном сайте OpenStreetMap.
Для того, чтобы на карту можно было вносить локальную информацию, можно настроить локальный веб-сайт 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.
Кроме заливки информации в локальную базу данных, я также пользуюсь и основной базой данных Mapnik, непосредственно залезая в её недра с помощью SQL-запросов. Поэтому мне в её таблицах бывают нужны некоторые атрибуты объектов, которые по умолчанию не импортируются в базу данных утилитой osm2pgsql.
Чтобы указать дополнительные поля, нужно отредактировать файл стиля базы данных /usr/share/osm2pgsql/default.style
Например, я добавил в файл стиля базы данных колонки addr:city и addr:street, которые берутся из одноимённых атрибутов объектов из файла OSM:
node,way addr:city text linearnode,way addr:street text linear
node означает, что этот атрибут может быть назначен точке и должен быть импортирован в таблицу planet_osm_point.
way означает, что этот атрибут может быть назначен контуру (линии, дороге или многоугольнику) и должен быть импортирован в таблицу planet_osm_line, planet_osm_roads или planet_osm_polygon.
Теперь информацию из базы данных можно извлекать с помощью SQL-запросов, в чём особенно помогают различные функции PostGIS.
Вот лишь краткий список функций, которые оказались полезными для моих задач:
Для чего можно использовать эти функции? Приведу несколько примеров, иллюстрирующих, как их использую я.
Например, для того, чтобы удалить из таблицы 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 узнать, в административные границы какого населённого пункта этот объект попадает.
Эту информацию без дополнительной обработки можно использовать для обратного геокодинга, то есть для получения адреса здания по географическим координатам точки, попавшей в контур здания:
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 городах, то эти особенности я учитывать не стал.
Нормализация номера дома:
Из неучтённых особенностей тут могут быть попытки вставить в поле номера дома слово "дом" или сокращение "д.", могут быть присутствовать слова "корпус", "корп.", "строение", "стр.", попытки вместо знака дроби написать слово "дробь" и т.п.
Нормализация названия улицы:
Также при поиске дома по адресу следует учитывать, что существуют угловые дома, которым часто назначаются сразу два адреса. В проекте OpenStreetMap нет единого соглашения по тому, каким образом в базе данных указывать такие адреса. Есть несколько разных подходов, которые описаны на этой странице: Key:addr. Угловые дома
Для отображения информации на карте я использую JavaScript-библиотеку LeafLet, написанную киевским программистом Владимиром Агафонкиным. Эта библиотека отстаёт по возможностям от библиотеки OpenLayers, которая используется самим проектом OpenStreetMap, но мне она понравилась компактностью и простотой использования.