воскресенье, 23 декабря 2012 г.

Postadmin 2

Ранее я уже писал о собственном веб-интерфейсе Postadmin для управления почтовым сервером. Писал я его с двоякой целью. Во-первых, я хотел приобрести опыт написания веб-приложений, а во-вторых, расширить возможности почтовой системы - добавить аутентификацию пользователей, сложные схемы пересылок, квоты для почтовых ящиков, ограничения на отправку.

При написании веб-интерфейса я пошёл по пути наименьшего сопротивления. Поскольку на тот момент я обладал довольно слабым знанием SQL, то решил не разрабатывать структуру таблиц с нуля, а воспользоваться имеющейся структурой, при необходимости добавляя новые поля и таблицы. Также, поскольку у меня в прошлом уже был опыт написания приложений на PHP, я решил воспользоваться именно им. Эти два решения позволяли мне увеличить шансы на успешное достижение целей.

По ходу реализации у меня сложилось некое подобие фреймворка - диспетчер обработки запросов, библиотечка для доступа к БД, зародыш шаблонизатора, несколько виджетов. Сейчас я смотрю на эту программу с изрядной долей самоиронии. Нет, она прекрасно работает, сам я легко в ней ориентируюсь, однако всё это похоже на детские каракули.

В начале года я стал осваивать Perl и начал пользоваться первым попавшимся шаблонизатором HTML::Template. Для работы с базами данных я освоил практически не имеющий альтернатив модуль DBI, что, впрочем, не так уж и плохо - DBI весьма хорош. Из трёх найденных мной веб-фреймворков, Catalyst, Mojolicious и Dancer, я решил остановиться на Dancer, как на наиболее простом из всех них. Это, конечно, не самый лучший набор инструментов, но для многих задач он вполне достаточен, а по сравнению с моим самописным недофреймворком на PHP он бесспорно лучше.

Итак, для закрепления навыков владения перечисленными инструментами (Perl, HTML::Template, DBI, Dancer), я решил портировать на них Postadmin. Функционал по синхронизации информации с порталом SharePoint я решил не воспроизводить, т.к. в настоящее время мне негде его отлаживать. Впрочем, почтового сервера у меня теперь тоже нет, поэтому в программе могут присутствовать некоторые скрытые недоработки. Если (вдруг!) кто-то решит воспользоваться этой программой и обнаружит недоработку, я готов её исправить.

Снимков экрана не привожу, так как шаблоны страниц фактически те же самые. Получившийся архив можно взять здесь. Вместе с самим веб-интерфейсом в архиве есть примеры настройки базы данных, веб-сервера, postfix и dovecot и небольшая инструкция.

воскресенье, 4 ноября 2012 г.

Подключение к базе данных с помощью Dancer::Plugin::Database

При написании веб-приложений обычно используется база данных. Безусловно, для подключения к базе данных можно воспользоваться обычным для Perl модулем DBI. Однако в Perl-фреймворке Dancer для этих целей есть удобная обёртка - плагин Database.

Поставим соответствующий модуль:
# apt-get install libdancer-plugin-database-perl
Перейдём в каталог с нашим Dancer-приложением, откроем файл конфигурации config.yml и впишем туда настройки подключения к базе данных:
plugins:
  Database:
    driver:                     "mysql"
    database:                   "db"
    host:                       "localhost"
    port:                       3306
    username:                   "user"
    password:                   "password"
    connection_check_threshold: 10
    on_connect_do:              "SET CHARACTER SET 'UTF8'"
Настройка connection_check_threshold задаёт частоту проверки доступности подключения к базе данных. Если последняя проверка доступности подключения проводилась более 10 секунда назад, то проводится новая проверка. Если было обнаружено, что подключение к базе данных пропало, оно будет переустановлено.

Сразу после подключения к базе данных выполнится команда (или несколько команд), указанная в настройке on_connection_do.

Плагин позволяет указать объект, который будет обрабатывать запросы к базе данных вместо используемого по умолчанию DBI::db. Можно определить несколько подключений к базе данных или назначить обработчики ошибок. В плагине определено несколько методов, позволяющих в простейших случаях обходиться без написания SQL-запросов.

Для использования плагина в основной модуль нашего веб-приложения lib/test.pm нужно прописать строчку подключения модуля:
use Dancer::Plugin::Database;
Теперь для выполнения запросов к базе данных можно использовать методы, обычные для модуля DBI:
my $sth = database->prepare("SELECT COUNT(*) FROM users");
Более подробно о плагине написано в его документации:
$ perldoc Dancer::Plugin::Database
Напоследок приведу картинку, не иллюстрирующую ничего из сказанного, однако сформированную с использованием описанного плагина и рассмотренного ранее модуля Dancer::Template::HtmlTemplate:

воскресенье, 28 октября 2012 г.

Удобная FreeBSD

Я привык к Linux, однако на работе приходится пользоваться FreeBSD. Чтобы работать в ней стало более-менее удобно, мне нужна поддержка UTF-8, привычные vim-lite вместо классического vi и bash вместо sh или csh.

Отредактируем файл /etc/login.conf, чтобы описание класса russian приняло следующий вид:
russian|Russian Users Accounts:\
:charset=UTF-8:\
:lang=ru_RU.UTF-8:\
:tc=default:
Перекомпилируем файл:
# cap_mkdb /etc/login.conf
И назначим себе класс russian:
# pw usermod -n mylogin -L russian
Теперь поставим bash и vim-lite из портов:
# cd /usr/ports/shells/bash
# make install
# cd /usr/ports/editors/vim-lite
# make install
Убедимся, что bash появился в списке доступных оболочек в файле /etc/shells, и при необходимости добавим туда строчку с полным путём к выполняемому файлу bash:
/usr/local/bin/bash
Теперь выберем себе оболочку с помощью команды chsh (change shell), прописав полный путь к bash в соответствующей строчке:
$ chsh
Осталось настроить вызов vim по команде vi. Для этого в файле ~/.profile пропишем псевдоним:
alias vi='vim'
Заодно выставим переменную PAGER в значение less (less позволяет листать обратно):
PAGER=less
Теперь при подключении по ssh мы получаем консоль, работающую в UTF-8, привычный bash с удобным автодополнением, поддержку UTF-8 в редакторе, вызываемом по команде vi (родной vi из FreeBSD не умеет работать с UTF-8, редактор ee - тоже) и поддержку визуального режима редактирования в нём.

Для настройки UTF-8 в системной консоли FreeBSD можно воспользоваться заметкой UTF-8 в консоли FreeBSD.

Другие полезные настройки описаны здесь Русификация FreeBSD (UTF-8).

воскресенье, 21 октября 2012 г.

Настройка тайлового сервера TileStache

Ещё один тайловый сервер, который я нашёл в дистрибутиве Debian - это TileStache. Он, как и TileLite, тоже написан на Python. Это ещё один комбайн "всё в одном": он поддерживает отрисовку тайлов с помощью Mapnik, кэширование тайлов, в том числе сторонних, и содержит встроенный веб-сервер (по сути он им и является). В отличие от TileLite, этот тайловый сервер может обслуживать несколько слоёв сразу, в том числе, может брать тайлы слоя из стороннего тайлового сервера.

Настройка в режиме самостоятельного веб-сервера

Итак, ставим его:
# apt-get install tilestache
Создаём сценарий инициализации /etc/init.d/tilestache:
#!/bin/sh

### BEGIN INIT INFO
# Provides:          tilestache
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts TileStache server
# Description:       starts TileStache server using start-stop-daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
NAME=tilestache
PID=/var/run/tilestache.pid
DAEMON=/usr/bin/tilestache-server
DAEMON_OPTS="--config /etc/tilestache.cfg -p 8000"

test -x $DAEMON || exit 0

set -e

case "$1" in
  start)
        echo "Starting $NAME: "
        start-stop-daemon --start --make-pidfile --background --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "done."
        ;;
  stop)
        echo "Stopping $NAME: "
        start-stop-daemon --stop --pidfile $PID --retry 5
        rm -f $PID
        echo "done."
        ;;
  restart)
        echo "Stopping $NAME: "
        start-stop-daemon --stop --pidfile $PID --retry 5
        rm -f $PID
        echo "done..."
        sleep 1
        echo "Starting $NAME: "
        start-stop-daemon --start --make-pidfile --background --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "done."
        ;;
  *)
        echo "Usage: /etc/init.d/$NAME {start|stop|restart}" >&2
        exit 1
        ;;
esac

exit 0

И прописываем в автозапуск:
# update-rc.d tilestache defaults
Копируем пример настройки сервера из файла /usr/share/doc/tilestache/examples/tilestache.cfg в файл /etc/tilestache.cfg и правим его до следующего вида:
{
  "cache":
  {
    "name": "Disk",
    "path": "/var/lib/tilestache",
    "umask": "0000"
  },
  "layers": 
  {
    "osm":
    {
        "provider": {"name": "mapnik", "mapfile": "/etc/mapnik-osm-data/osm.xml"},
        "projection": "spherical mercator"
    } 
  }
}
Создаём каталог для кэша тайлов:
# mkdir /var/lib/tilestache
Осталось запустить TileStache:
# /etc/init.d/tilestache start
Настройка Lighttpd для проксирования запросов к TileStache

Включаем модуль proxy веб-сервера Lighttpd:
# lighty-enable-mod proxy
Настраиваем модуль на проксирование каталога /osm в файле конфигурации модуля proxy /etc/lighttpd/conf-enabled/10-proxy.conf:
server.modules   += ( "mod_proxy" )
proxy.server = (
  "/osm" =>
  (
    (
      "host" => "127.0.0.1",
      "port" => 8000
    )
  )
)
Будьте внимательны, после строки /osm не должно быть косой черты, иначе тайловый сервер не будет работать должным образом.

Теперь ерезапустим Lighttpd:
# /etc/init.d/lighttpd restart
Настройка в режиме FastCGI для работы с Lighttpd

Теперь повторим уже опробованную ранее на TileLite методику запуска тайлового сервера в режиме FastCGI.

Создадим каталог для настроек TileStache, перенесём в него наш конфиг и поставим flup:
# mkdir /etc/tilestache
# mv /etc/tilestache.cfg /etc/tilestache
# apt-get install python-flup
В качестве образца для FastCGI-сервера возьмём файл /usr/bin/tilestache-server, скопировав его в каталог /etc/tilestache под именем tilestache.py:
# cp /usr/bin/tilestache-server /etc/tilestache/tilestache.py
Отредактируем его до следующего состояния:
#!/usr/bin/python

if __name__ == '__main__':
    from flup.server.fcgi import WSGIServer
    import TileStache

    application = TileStache.WSGITileServer(config='/etc/tilestache/tilestache.cfg', autoreload=True)
    WSGIServer(application).run()
Дадим права на выполнение:
# chmod +x /etc/tilestache/tilestache.py
И приступим к настройке модуля FastCGI в Lighttpd. Включим его:
# lighty-enable-mod fastcgi
И настроим следующим образом:
$HTTP["url"] =~ "^/osm/" {
  fastcgi.server +=
  (
    "" => 
    (
      (
        "bin-path" => "/etc/tilestache/tilestache.py",
        "socket" => "/tmp/tilestache.socket",
        "max-procs" => 1,
        "check-local" => "disable",
      )
    )
  )
}
В отличие от TileLite, этот сервер может обслуживать несколько слоёв одновременно, поэтому имя слоя нужно передавать в ссылке на запрашиваемый тайл. Поэтому настройка столь необычна. Она передаёт на обслуживание FastCGI-процессу корень веб-сервера лишь в том случае, если запрошена страница, адрес которой начинается с /osm/, то есть - имени слоя. Если нужно настроить несколько слоёв, потребуется доработать регулярное выражение таким образом, чтобы оно совпадало с именем каждого из слоёв.

Поскольку теперь TileStache будет запускаться Lighttpd, настроим права на доступ к каталогу /var/lib/tilestache:
# chmod www-data:www-data /var/lib/tilestache
Теперь остановим и отключим настроенный ранее TileStache, отключим модуль proxy и перезапустим веб-сервер:
# /etc/init.d/tilestache stop
# update-rc.d tilestache disable
# lighty-disable-mod proxy
# /etc/init.d/lighttpd restart
Нет в мире совершенства. Этот сервер хотя и настраивается легко и работает очень устойчиво, но работает во-первых очень медленно, а во-вторых, имеет утечки памяти. Поэтому для использования в нынешнем виде он не пригоден.

Количество, увы, не перешло в качество. Интересно, пользуются ли авторы своими творениями? Пользуются ли мэйнтейнеры из Debian GIS Project поддерживаемыми ими пакетами? Судя по отсутствию сценариев инициализации - нет. А если и пользуются, то скорее всего совместно с Apache и mod_wsgi. Но в таком случае я их не понимаю - раз уж от Apache никуда не деться, почему бы не воспользоваться mod_tile?

воскресенье, 14 октября 2012 г.

Настройка тайлового сервера TileLite

Ранее я уже описывал настройку системы для отрисовки тайлов OpenStreetMap на основе renderd, apache и mod_tile в своей заметке Установка renderd и mod_tile - системы отрисовки тайлов по запросу.

Сейчас же я хочу рассмотреть ещё одни тайловый сервер, основанный на Mapnik - сервер TileLite. Но прежде чем я это сделаю, хочу сразу сказать о достоинствах и недостатках обеих систем.

Система из renderd, apache и mod_tile хороша тем, что она представляет собой законченное решение, так как включает в себя функции отрисовки тайлов, кэширования тайлов и веб-сервер для отдачи тайлов. Ещё одно преимущество этой связки заключается в том, что она способна обслуживать сразу несколько слоёв. Недостатков у неё два - она сравнительно сложна, громоздка и отсутствует в официальном репозитории Debian.

Система на основе TileLite сочетает в рамках одного процесса функции отрисовки тайлов, функцию кэширования и веб-сервер. Преимущество этой системы - простота её настройки и наличие в официальном репозитории Debian. Недостатки - система умеет обслуживать только один слой, при этом довольно требовательна к ресурсам и на моём компьютере работала не очень стабильно.

Настройка в режиме самостоятельного веб-сервера

Поставим пакет tilelite:
# apt-get install tilelite
Создадим сценарий инициализации /etc/init.d/liteserv со следующим содержимым:
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts TileLite server
# Description:       starts TileLite server using start-stop-daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
NAME=liteserv
PID=/var/run/liteserv.pid
DAEMON=/usr/bin/liteserv
DAEMON_OPTS="/etc/mapnik-osm-data/osm.xml --config /etc/liteserv.cfg"

test -x $DAEMON || exit 0

set -e

case "$1" in
  start)
        echo "Starting $NAME: "
        start-stop-daemon --start --make-pidfile --background --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "done."
        ;;
  stop)
        echo "Stopping $NAME: "
        start-stop-daemon --stop --pidfile $PID --retry 5
        rm -f $PID
        echo "done."
        ;;
  restart)
        echo "Stopping $NAME: "
        start-stop-daemon --stop --pidfile $PID --retry 5
        rm -f $PID
        echo "done..."
        sleep 1
        echo "Starting $NAME: "
        start-stop-daemon --start --make-pidfile --background --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
        echo "done."
        ;;
  *)
        echo "Usage: /etc/init.d/$NAME {start|stop|restart}" >&2
        exit 1
        ;;
esac

exit 0

И добавим скрипт в автозапуск:
# update-rc.d liteserv defaults
Теперь скопируем пример файла конфигурации в каталог /etc:
# cp cp /usr/share/doc/tilelite/examples/tilelite.cfg /etc/liteserv.cfg
И поправим его до следующего состояния:
[tiles]
size = 256
buffer_size = 128
format = png
paletted = no
max_zoom = 22
debug = off
watch_mapfile = off
watch_interval = 2
max_failures = 6

[cache]
caching = on
cache_path = /var/lib/liteserv
cache_force = off
Создадим каталог для хранения кэша (почему-то он работает, только если указать соответствующую опцию командной строки):
# mkdir /var/lib/liteserv
И запустим его:
# /etc/init.d/liteserv start
Сервер будет ожидать подключений на локальном адресе 127.0.0.1, на TCP-порту 8000. Для изменения настроек можно указать соответствующие опции в сценарии init.d.

Настройка Lighttpd для проксирования запросов к TileLite

Можно настроить веб-сервер Lighttpd для проксирования запросов к TileLite:
server.modules   += ( "mod_proxy" )
proxy.server = (
  "/osm/" =>
  (
    (
      "host" => "127.0.0.1",
      "port" => 8000
    )
  )
)
Этот фрагмент можно вписать прямо в файл /etc/lighttpd/lighttpd.conf, но в Debian правильнее будет включить модуль proxy и вписать настройки в его файл конфигурации /etc/lighttpd/conf-enabled/10-proxy.conf. Для включения модуля используется следующая команда:
# lighty-enable-mod proxy
Можно поставить ещё пакет python-werkzeug и настроить количество одновременно работающих процессов TileLite в сценарии init.d. Хотя с такой настройкой TileLite у меня работал быстро, но работал он не долго - всего через несколько секунд он переставал отвечать на запросы.

Настройка в режиме FastCGI для работы с Lighttpd

Вместо этого я попробовал ещё один подход - запускать TileLite как сервер FastCGI. Для этого нужно поставить пакет python-flup:
# apt-get install python-flup
Теперь создадим скрипт для запуска TileLite как FastCGI-процесса и настроим Lighttpd. Перед созданием скрипта запуска я создал каталог /etc/liteserv, в который перенёс настроенный ранее конфиг liteserv.cfg из каталога /etc. Воспользовавшись примером WSGI-скрипта из каталога /usr/share/doc/tilelite/examples/tiles_app.py и примерами настройки FastCGI на сайте сервера Lighttpd, я наваял следующий FastCGI-скрипт для TileLite, который разместил в файле /etc/liteserv/liteserv.py:
#!/usr/bin/python

from tilelite import Server
from flup.server.fcgi_fork import WSGIServer

mapfile = '/etc/mapnik-osm-data/osm.xml'
config = '/etc/liteserv/liteserv.cfg'

application = Server(mapfile,config)
WSGIServer(application, maxChildren=3).run()
Две особенности этого скрипта - это использование flup.server.fcgi_fork вместо более обычного flup.server.fcgi и указание опции maxChildren=3. Они заставляют FastCGI-сервер работать в многопроцессном, а не в многопоточном режиме, при чём число одновременно работающих процессов не должно превышать 3. Эти настройки позволили мне достичь наибольшей стабильности в работе тайлового сервера на своём старом компьютере. Похоже, что моему компьютеру не хватает оперативной памяти, чтобы обрабатывать много запросов к базе данных сразу, а сам TileLite плохо работает в многопоточном режиме.

Теперь настроим Lighttpd. Для этого нужно включить модуль fastcgi:
# lighty-enable-mod fastcgi
И прописать в файл /etc/lighttpd/conf-enabled/10-fastcgi.conf следующие настройки:
fastcgi.server = (
  "/osm" => 
  (
    (
      "bin-path" => "/etc/liteserv/liteserv.py",
      "socket" => "/tmp/liteserv.socket",
      "max-procs" => 1,
      "check-local" => "disable"
    )
  )
)
Очень важно не дописывать к строчке /osm символа косой черты в конце - от этого TileLite сходит с ума и не обрабатывает запросы должным образом.

Наконец, поскольку теперь TileLite будет запускаться Lighttpd, настроим права на доступ к каталогу /var/lib/liteserv:
# chmod www-data:www-data /var/lib/liteserv
И перезапустим сервер, не забыв отключить настроенный до этого liteserv и настройку модуля proxy Lighttpd:
# /etc/init.d/liteserv stop
# update-rd.d liteserv disable
# lighty-disable-mod proxy
# /etc/init.d/lighttpd restart
В таком виде тайловый сервер в режиме FastCGI заработал более-менее стабильно.

Однако, поскольку этот тайловый сервер закидывает запросами PostgreSQL и не делает запросы повторно в случае ошибки, на моём компьютере TileLite работает довольно нестабильно. Поэтому я всё-же предпочёл продолжить использование renderd, apache и mod_tile.

воскресенье, 30 сентября 2012 г.

Хранение сессий в MySQL в Perl-фреймворке Dancer

Для хранения сессий Dancer в базах данных существует модуль Dancer::Session::DBI. В настоящее время он поддерживает хранение сессий лишь в MySQL и SQLite. В Debian этот модуль отсутствует, поэтому поставим его с помощью dh-make-perl:
# dh-make-perl --install --cpan Dancer::Session::DBI
Также этому модулю понадобится пакет libjson-perl (фактически это модуль JSON для Perl), для того, чтобы сохранять переменные сессии в поле таблицы MySQL.
# apt-get install libjson-perl
Теперь создадим в базе данных таблицу для хранения сессий. Судя по документации, у неё должно быть два обязательных поля: id - из 40 символов и session_data - текстовое поле, в которое и будут сохранятся переменные сессии. Также полезно добавить поле last_active, которое будет содержать дату и время последнего обновления записи. Это поле можно использовать для периодического удаления устаревших сессий. Создадим таблицу:
mysql >
CREATE TABLE `session` (
  `id` char(40) NOT NULL,
  `session_data` text,
  `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Осталось настроить наше приложение на использование установленного модуля сессий и можно начинать им пользоваться. Настроим приложение, добавив следующие строчки в файл config.yml в каталоге проекта:
session: "DBI"
session_options:
  dsn:      "DBI:mysql:database=base;host=localhost;port=3306"
  table:    "session"
  user:     "session"
  password: "session"

serializer: "JSON"
Теперь создадим шаблон страницы, который будет использоваться для запроса имени пользователя и пароля для аутентификации. Шаблон назовём login.tt и положим в каталог views проекта:
<div align="center">
  <form method="POST">
    <table>
      <tr>
        <td>
          <label for="login">Логин:</label>
        </td>
        <td>
          <input type="text" id="login" name="login">
        </td>
      </tr>
      <tr>
        <td>
          <label for="password">Пароль:</label>
        </td>
        <td>
          <input type="password" id="password" name="password">
        </td>
      </tr>
      <tr>
        <td colspan="2" style="text-align: center;">
          <input type="submit" name="ok" value="Войти">
        </td>
      </tr>
    </table>
  </form>
</div>
И создадим шаблон страницы, которая будет показываться только пользователям, прошедшим аутентификацию:
<h3 align="center"><TMPL_VAR NAME="name"></h3>
<div align="center"><a href="/logout">Выйти</a></div>
Теперь опишем поведение приложения, которое будет показывать эти страницы и обрабатывать ввод пользователя. Откроем файл test.pm в каталоге lib и добавим туда обработчики GET- и POST-запросов:
get '/login' => sub {
  template 'login';
};

post '/login' => sub {
  my $login = param "login";
  my $password = param "password";

  if (($login eq "stupin") && ($password eq "test"))
  {
    session user_id => 1;
    session name => "Владимир Ступин";
  }
  redirect '/restricted';
};

get '/logout' => sub {
  session->destroy();
  redirect '/login';
};

get '/restricted' => sub {
  if (not session('user_id')) {
    redirect '/login';
  }
  template 'restricted', { name => session('name') };
};
Здесь есть два обработчика страницы по адресу /login - первый просто выводит шаблон страницы аутентификации, второй - проверяет введённые логин и пароль, создаёт новую сессию и переадресует посетителя на страницу с ограниченным доступом /restricted, если логин и пароль введены правильно.

Обработчик страницы по адресу /logout. При попадании на неё сессия завершается, а посетитель перенаправляется на страницу /login.

Обработчик страницы по адресу /restricted проверяет, что имеется активный сеанс, в котором есть переменная с именем user_id. Если такой переменной или сессии нет, пользователь переадресуется на страницу входа /login. Если всё в порядке, то пользователю показывается страница с информацией для аутентифицированных пользователей.

К сожалению, не всё с модулем Dancer::Session::DBI оказалось так гладко. Пришлось немного доработать его напильником, чтобы русские буквы сохранялись в базе данных в правильной кодировке и чтобы он не выдавал ошибку, если пользователь приходит с идентификатором сессии, которой нет в базе.

Первая доработка в файле /usr/share/perl5/Dancer/Session/DBI.pm выглядит следующим образом:
sub _dbh {
    my $self = shift;
    my $settings = setting('session_options');

    # Prefer an active DBH over a DSN.
    return $settings->{dbh}->() if defined $settings->{dbh};

    # Check the validity of the DSN if we don't have a handle
    my $valid_dsn = DBI->parse_dsn($settings->{dsn} || '');

    die "No valid DSN specified" if !$valid_dsn;

    if (!defined $settings->{user} || !defined $settings->{password}) {
        die "No user or password specified";
    }

    # If all the details check out, return a fresh connection
    my $dbh = DBI->connect($settings->{dsn}, $settings->{user}, $settings->{password});
    if ((defined $dbh) && (defined $settings->{charset}))
    {
      my $sth = $dbh->prepare("SET CHARACTER SET ?");
      $sth->execute($settings->{charset});
      $sth->finish();

      $dbh->{mysql_enable_utf8} = 1 if $settings->{charset} eq "UTF8";
    }
    return $dbh;
}
Вторая доработка в этом же файле выглядит следующим образом:
sub retrieve {
    my ($self, $session_id) = @_;

    my $session = try {
        my $quoted_table = $self->_quote_table;

        my $sth = $self->_dbh->prepare_cached(qq{
            SELECT session_data
            FROM $quoted_table
            WHERE id = ?
        });

        $sth->execute( $session_id );
        my ($session) = $sth->fetchrow_array();
        $sth->finish();

        $session = "{}" unless defined $session;
        $self->_deserialize($session);
    } catch {
        warning("Could not retrieve session ID $session_id - $_");
        return;
    };

    return bless $session, __PACKAGE__ if $session;
}
Первая доработка позволяет указать кодировку клиента, которую клиент должен установить сразу после подключения к базе данных. Настроим кодировку в файле настройки проекта config.yml, вместе с ней настройки модуля сессий примут следующий вид:
session: "DBI"
session_options:
  dsn:      "DBI:mysql:database=base;host=localhost;port=3306"
  table:    "session"
  user:     "session"
  password: "session"
  charset:  "UTF8"
Само собой, это приложение лишь демонстрирует использование сессий в Dancer. В реальном приложении нужно написать функции аутентификации, использующие таблицу пользователей и функции, ограничивающие доступ к страницам и операциям, использующие таблицы групп пользователей и их прав. Напоследок, пара снимков экрана с двумя страницами:

воскресенье, 23 сентября 2012 г.

Настройка nginx

Решил попробовать веб-сервер nginx вместо более привычного мне lighttpd. Разница между ними, на мой взгляд, как между правым и левым ботинком, но тем не менее, первый, судя по проникающему в мои уши информационному шуму, почему-то более популярен.

Поставим нужные нам пакеты nginx и spawn-fcgi:
# apt-get install nginx spawn-fcgi
Главный файл конфигурации - /etc/nginx/nginx.conf. В нём много комментариев, но выжимку из него получить можно с помощью простой команды grep -vE "^\s*$|^\s*#" /etc/nginx/nginx.conf. После моей небольшой доработки, в которой я снизил количество процессов с 4 до 1, файл принял следующий вид:
user www-data;
worker_processes 1;
pid /var/run/nginx.pid;
events {
  worker_connections 768;
}
http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}
Система, на которой я настраиваю этот веб-сервер - домашняя и не предполагает высокой нагрузки. Больше в этом конфиге я ничего не менял.

Следующий интересующий меня файл - это, как ни странно, сценарий инициализации процесса spawn-fcgi, родственного веб-серверу lighttpd, который будет обслуживать php-страницы. Его я взял из своей заметки Шпаргалка по настройке веб-сервера Lighttpd и доработал под мой случай. Поскольку веб-сервер будет работать с бэкэндом на одном компьютере, для общения между ними выгоднее использовать Unix-сокет, а не TCP-соединение. Также зададим 4 процесса-бэкэнда, так что одновременно может исполняться до 4 скриптов, формирующих страницы, запрошенные клиентами:
#!/bin/sh

### BEGIN INIT INFO
# Provides:          spawn-fcgi-php
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts FastCGI for PHP
# Description:       starts FastCGI for PHP using start-stop-daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
NAME=spawn-fcgi-php
PID=/var/run/spawn-fcgi-php.pid
DAEMON=/usr/bin/spawn-fcgi
#DAEMON_OPTS="-f /usr/bin/php-cgi -a 127.0.0.1 -p 8080 -u www-data -g www-data -P $PID"
DAEMON_OPTS="-f /usr/bin/php-cgi -s /var/run/php.sock -F 4 -u www-data -g www-data -P $PID"

test -x $DAEMON || exit 0

set -e

case "$1" in
  start)
    echo "Starting $NAME: "
    start-stop-daemon --start --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
    echo "done."
    ;;
  stop)
    echo "Stopping $NAME: "
    start-stop-daemon --stop  --pidfile $PID --retry 5
    rm -f $PID
    echo "done."
    ;;
  restart)
    echo "Stopping $NAME: "
    start-stop-daemon --stop  --pidfile $PID --retry 5
    rm -f $PID
    echo "done..."
    sleep 1
    echo "Starting $NAME: "
    start-stop-daemon --start --pidfile $PID --exec $DAEMON -- $DAEMON_OPTS
    echo "done."
    ;;
  *)
    echo "Usage: /etc/init.d/$NAME {start|stop|restart}" >&2
    exit 1
    ;;
esac

exit 0
Теперь добавим этот скрипт в автозагрузку и приступим к самому интересному - к собственно настройке веб-сервера:
# chmod +x /etc/init.d/spawn-fcgi-php
# update-rc.d spawn-fcgi-php defaults
Создадим в каталоге /etc/nginx/sites-available/ файл mysite со следующим содержимым:
server {
  root /var/www/;
  index index.php index.html index.htm;
  server_name localhost;
  location /osm/ {
    proxy_pass http://127.0.0.1:8080/osm/ ;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
  location / {
    try_files $uri $uri/ / ;
  }
  location /doc/ {
    alias /usr/share/doc/;
    autoindex on;
    allow 127.0.0.1;
    allow ::1;
    deny all;
  }
  location /postadmin/ {
    auth_basic "postadmin";
    auth_basic_user_file /etc/nginx/htpasswd;
  }
  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php.sock;
    fastcgi_index index.php;
    include fastcgi_params;
  }
}
Этот файл показывает сразу несколько интересных (по крайней мере мне) примеров настройки. Из него видно, как создать псевдоним каталога, ограничить доступ по IP-адресам, ограничить доступ по паролю, обслуживать часть сайта, перенаправляя запросы другому веб-серверу, и исполнять php-скрипты, направляя запросы ранее настроенному процессу spawn-fcgi-php (аналогично spawn-fcgi можно использовать для управления другими fastcgi-процессами, например, написанными на Perl).

Переключим nginx на использование созданного файла конфигурации:
# unlink /etc/nginx/sites-enabled/default
# ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite
Думаю, что настройки не нуждаются в особых пояснениях. Часть из них понятна без комментариев, а часть является чёрной магией, специфичной для nginx.

Создать файл паролей можно с помощью утилиты htpasswd из пакета apache2-utils:
$ htpasswd -c /etc/nginx/htpasswd stupin
И запустим веб-сервер:
# /etc/init.d/nginx start
По итогам настройки могу сделать следующие неутешительные для nginx выводы:
1. Он не умеет самостоятельно порождать и прибивать локальные fastcgi-процессы, как это умеет делать lighttpd,
2. Он не поддерживает digest-авторизацию, что называется "из коробки". Существует модуль, поддерживающий digest-авторизацию, но он распространяется отдельно от nginx и не пакетирован в Debian.
3. Он не имеет родственного пакета, управляющего fastcgi-процессами, поэтому необходимый spawn-fcgi приходится брать из отдельного пакета, который изначально делался вместе с веб-сервером lighttpd.

Не то что бы это плохой веб-сервер, но мне он показался немного сыроват по сравнению с используемым мной lighttpd, которым я пользуюсь уже почти 5 лет. В плюс nginx'у можно записать лёгкость настройки.

Интересно будет услышать мнение людей, использующих nginx, которым по каким-то разумным причинам не подошёл lighttpd.

воскресенье, 16 сентября 2012 г.

Шаблоны HTML::Template в Perl-фреймворке Dancer

Захотелось немного изучить веб-фреймворк Dancer, т.к. писать с использованием модуля CGI и его компании хоть и можно, но не удобно. Особенно раздражает неудобство в использовании cookie, а ещё больше - неудобство установки или удаления cookie в редиректе. Хотя, конечно, этим всё не ограничивается.

Ставим фреймворк:
# apt-get install libdancer-perl
Создаём каркас будущего приложения под названием test:
$ dancer -a test
Теперь можно перейти в каталог test/bin и запустить оттуда приложение app.pl:
$ cd test/bin
$ ./app.pl
Запустится только что созданное приложение test, которое будет ожидать подключений на TCP-порту 3000. Можно открыть в браузере ссылку http://localhost:3000 и увидеть тестовую страницу:

Как вы уже догадались по заголовку заметки, необычное в этом снимке экрана только одно - строчка "Template engine: html_template".

По умолчанию в Dancer'е используется некий "простой" шаблонизатор simple. В качестве альтернативы предлагается использовать template_toolkit, я же хочу воспользоваться html_template, который по сегодняшним временам кому-то может показаться довольно "дубовым".

К сожалению, в репозитории Debian этого модуля нет. Поэтому поставим сначала пакет dh-make-perl:
# apt-get install dh-make-perl
Теперь с его помощью поставим нужный нам Perl-модуль Dancer::Template::HtmlTemplate, соглашаясь при первом запуске на все умолчальные настройки:
# dh-make-perl --install --cpan Dancer::Template::HtmlTemplate
Теперь укажем Dancer'у, что мы хотим использовать этот шаблонизатор в нашем приложении. Для этого закомментируем в файле настроек config.yml в каталоге проекта строчку шаблонизатора simple и добавим строчку с шаблонизатором html_template:
#template: "simple"
template: "html_template"
Теперь самое время отредактировать шаблоны в соответствии с правилами выбранного нами шаблонизатора - заменим все строчки типа "<% var %>" на строчки типа "<TMPL_VAR NAME="var">". Шаблоны находятся в каталоге views проекта и имена их файлов оканчиваются расширением ".tt".

Теперь можно снова запустить наше тестовое веб-приложение и увидеть ту самую страницу, снимок которой я привёл выше.

Если заглянуть в исходный текст модуля, то можно увидеть, что опции HTML::Template, используемые при загрузке шаблона, можно настроить всё в том же файле настройки config.yml:
my $ht = HTML::Template->new(
  filename => $template,
  die_on_bad_params => 0, # Required, as we pass through other params too
  %{$self->config}
);
Я использую настройки utf8 и loop_context_vars. Их я и пропишу в файл config.yml:
engines:
  html_template:
    utf8:              1
    loop_context_vars: 1
Теперь создадим в каталоге views шаблон страницы hello, поместив в файл hello.tt следующий текст:
<h3 align="center">
<TMPL_VAR NAME="name">
</h3>
И откроем файл test.pm в каталоге lib, допишем в него обработчик страницы hello:
get '/hello/:name' => sub {
  template 'hello', { name => param('name') };
};
Теперь перейдём по ссылке "http://localhost:3000/hello/превед" и увидим следующее:

Страница создана на основе макета, лежащего в файле views/layouts/main.tt и созданного нами шаблона view/hello.tt. Если захочется поменять что-то в макете - можно отредактировать его, можно создать новый макет и положить его рядом. Выбрать используемый макет можно прямо в обработчике. В документации приведены два примера.

Отключение макета на определённой странице:
get '/' => sub {
  template 'index', {}, { layout => undef };
};
Или использование указанного макета:
get '/user' => sub {
  template 'user', {}, { layout => 'user' };
};
На этом пока всё.

воскресенье, 9 сентября 2012 г.

Perl и UTF-8

Оставим в стороне теорию, о ней достаточно написано из без меня, например, здесь Пара слов про UTF-8 и здесь UTF Perl Practice, или как использовать UTF-8 в перле. После прочтения теории я занялся практикой и сделал для себя следующие практические заготовки.

Включаем флаг UTF-8 для всех строк в программе:
use utf8;
Выставляем флажок UTF-8 для всех строк, прочитанных или записанных в текстовые файлы, включая стандартные потоки ввода-вывода:
use open qw(:std :utf8);
Странным образом предыдущие настройки включают блочную буферизацию стандартных потоков вывода. Отключаем её:
select(STDERR);
$| = 1;
select(STDOUT);
$| = 1;
Выставляем флажок UTF-8 для всех строк, полученных в переменных CGI:
use CGI qw(:cgi -utf8);
Выставляем флажок UTF-8 для файла, с помощью которого грузится шаблон HTML::Template и все подшаблоны. К сожалению, не нашёл в документации модуля способа однократно задать использование этой настройки глобально.
my $template = HTML::Template->new(filename => "/var/www/index.tmpl",
                                   utf8 => 1,
                                   die_on_bad_params => 0,
                                   global_vars => 1);
Чтобы модуль DBI выставлял флаг UTF-8 для всех строк, прочитанных из БД MySQL или переданных ей, выставляем настройку в дескрипторе соединения.
$dbh->{mysql_enable_utf8} = 1;
Аналогично для PostgreSQL:
$dbh->{pg_enable_utf8} = 1;
Нужно отметить, что эти настройки для DBI не указывает кодировку клиента, а только лишь дают инструкцию модулю выставлять флажок UTF-8 у строк.

На этом, вроде, всё.

воскресенье, 2 сентября 2012 г.

Настройка vtun в Debian

Опишу настройку vtun для довольно экзотической задачи, которая тем не менее даёт хорошее представление о возможностях vtund.

Постановка задачи

Имеется две локальные сети network1 и network2 за шлюзами gateway1 и gateway2 соответственно. Шлюз gateway1 имеет "белый" IP-адрес, а второй шлюз может выходить в интернет только через NAT на вышестоящем шлюзе nat_gateway:
   _____________    ________                    _____________      ________       _____________
  /            /   |        |                  |             |    |        |     /            /
 /  network1  /----|gateway1|----/internet/----O nat_gateway |----|gateway2|----/  network2  /
/____________/     |________|                  |_____________|    |________|   /____________/
Требуется, чтобы сеть network1 могла ходить в сеть network2, но таким образом, чтобы узлы из сети network2 не знали о существовании каких-то других сетей, а считали, что с ними связывается непосредственно сам шлюз gateway2. То есть, необходимо на шлюзе gateway2 настроить трансляцию адресов network1 в локальный адрес шлюза gateway2.

Сеть network1: 10.0.0.1/24

Сеть network2: 192.168.0.0/24

Доменное имя gateway1 в интернете - server.domain.tld.

Настроим vtund на шлюзе gateway1 в режиме сервера

В файл /etc/default/vtund внесём следующую строчку:
RUN_SERVER=yes
В файле /etc/vtund.conf опишем конфигурацию сервера:
options {
  type stand;
  port 5000;

  bindaddr {
    iface eth0;
  };

  ifconfig /sbin/ifconfig;
  ip /sbin/ip;
  firewall /sbin/iptables;
}

vpn1 {
  passwd mysecret;
  type tun;
  proto tcp;
  compress no;
  encrypt yes;
  keepalive yes;
  stat yes;
  multi killold;

  up {
    ifconfig "%% 192.168.1.1 pointopoint 192.168.1.2";
    ip "route add 192.168.0.0/24 dev %%";
  };

  down {
    ifconfig "%% down";
    ip "route delete 192.168.0.0/24 dev %%";
  };
}
Настроим vtund на шлюзе gateway2 в режиме клиента

В файл /etc/default/vtund внесём следующие строчки:
CLIENT0_NAME="vpn1"
CLIENT0_HOST="server.domain.tld"
В файле /etc/vtund.conf опишем конфигурацию клиента:
options {
  type stand;
  port 5000;

  ifconfig /sbin/ifconfig;
  ip /sbin/ip;
  firewall /sbin/iptables;
}

vpn1 {
  passwd mysecret;
  type tun;
  proto tcp;
  compress no;
  encrypt yes;
  stat yes;
  persist yes;

  srcaddr {
    iface eth0;
  };

  up {
    ifconfig "%% 192.168.1.2 pointopoint 192.168.1.1";
    ip "route add 10.0.0.0/24 dev %%";
    firewall "-t nat -A POSTROUTING -o eth0 -j MASQUERADE";
  };

  down {
    ifconfig "%% down";
    ip "route delete 10.0.0.0/24 dev %%";
    firewall "-t nat -D POSTROUTING -o eth0 -j MASQUERADE";
  };
}
Описание работы настроенной системы

Итак, работать это будет следующим образом. Шлюз gateway2 устанавливает соединение со шлюзом gateway1, используя доменное имя сервера server.domain.tld, TCP-порт сервера 5000 и пароль mysecret. Между шлюзами устанавливается туннель точка-точка, через который шлюз gateway1 будет отправлять пакеты для сети network2 192.168.0.0/24, находящийся за шлюзом gateway2. В свою очередь, шлюз gateway2 будет отправлять пакеты для сети network1 10.0.0.0/24 через шлюз gateway1.

При этом, если узел сети network1 10.0.0.0/24 попытается связаться с узлом из сети network2 192.168.0.0/24, шлюз gateway2 будет транслировать адреса сети network1 10.0.0.0/24 в свой локальный адрес (в терминологии iptables это называется SNAT или MASQUERADE).

В случае обрыва соединения gateway2 будет самостоятельно переустанавливать подключение. gateway1, при этом, допускает подключение только одного клиента и все предыдущие подключения будут заменяться вновь установленными.

Особенности, достоинства и недостатки

Файл /etc/default/vtund допускает настройку установки до 10 клиентских соединений (легко увеличивается редактированием скрипта инициализации /etc/init.d/vtun). Количество соединений, принимаемых сервером, не ограничено - их можно настроить в файле /etc/vtund.conf. При этом vtund может быть одновременно клиентом для одной части соединений и сервером для другой части соединений.

Из недостатков можно отметить только один - для каждого соединения необходимо прописывать собственные настройки, т.к. vtund не поддерживает встроенную возможность раздавать клиентам адреса из заранее заданного пула. Теоретически это можно реализовать с помощью внешних скриптов. Я считаю, что это не очень большой недостаток, т.к. этот недостаток является оборотной стороной достоинства vtund - простоты настройки (в отличие, например, от openvpn).

При настройке vtund я воспользовался собственными переводами страниц руководства: vtund.conf(5) и vtund(8)

воскресенье, 26 августа 2012 г.

Проблема с кодировками в MySQL

Не смотря на то, что в настоящее время почти повсеместно используется кодировка UTF-8, ещё случаются иногда курьёзные случаи с кодировками.

Недавно столкнулся с проблемой такого рода. Сервер базы данных работал в latin1. Кодировка базы данных и всех её таблиц - UTF-8. На веб-сервере работало приложение на PHP, которое само по себе оперировало информацией в базе данных тоже в кодировке UTF-8. Беда в том, что в переменных окружения веб-сервера тоже стояла кодировка latin1. Сервер базы данных общался с клиентом базы данных, думая что оба они работают с информацией в кодировке latin1, хотя по факту информация была в UTF-8. Соответственно, никаких проблем не наблюдалось до тех пор, пока к серверу баз данных не подключился клиент, который захотел общаться в кодировке, отличной от latin1. Клиент получал что угодно, но только не текст в запрошенной кодировке.

Чтобы исправить ситуацию, в веб-приложении пришлось сразу после подключения к базе данных задавать желаемую кодировку запросом "SET CHARACTER SET 'UTF8'". Осталось перекодировать данные.

Сначала снимем дамп с базы данных, запросив данные в кодировке latin1. На самом деле они сольются в той кодировке, которой пользовалось веб-приложение. В данном случае это UTF-8.
$ mysqldump --default-character-set=latin1 -uroot -p base | grep -vE "^\/\*" > base.sql
Из дампа попутно удаляются все строки, начинающиеся с символов "/*" - это директивы, задающие настройки кодировок при импорте-экспорте.

Теперь запустим консольный клиент, и создадим пустую базу данных в кодировке UTF-8:
$ mysql -uroot -p
> create database base2 charset utf8;
Осталось выбрать кодировку, в которой хранится информация в дампе и залить в новую базу данных ранее сохранённый дамп. В нашем случае это опять UTF-8.
> charset utf8;
> use base2;
> source base.sql
Теперь можно выбирать в клиенте ту кодировку, которая стоит в терминале, и видеть текст. В моём случае в терминале была настроена кодировка KOI8-R:
> charset koi8r;
> select * from table_name limit 10;
Если всё сделано правильно, можно повторить восстановление дампа уже в основную базу и удалить тестовую базу данных:
> drop database base;
> create database base charset utf8;
> charset utf8;
> source base.sql
> drop database base2;

воскресенье, 19 августа 2012 г.

Тайловый сервер - что с ним делать?

В прошлых заметках Подготовка карт для генератора тайлов Mapnik, Настройка базы данных для генератора тайлов Mapnik, Установка генератора тайлов Mapnik и Установка renderd и mod_tile - системы отрисовки тайлов по запросу мы настроили обычный тайловый сервер. Однако, от одного лишь просмотра карт толку мало - ведь то же самое без лишних усилий можно увидеть и на официальном сайте OpenStreetMap.

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  linear
node означает, что этот атрибут может быть назначен точке и должен быть импортирован в таблицу 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-запросом.

Процедура нормализации у меня делится на три части, из которых самой сложной является нормализация названия улицы.

Нормализация названия населённого пункта:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е",
  4. Удаляются сокращения "г.", "п.", "с.", "д.", слова "город", "поселок", "село", "деревня".
Полученная строка используется для сравнения.

Из неучтённых особенностей тут могут быть одноимённые населённые пункты разного класса. Например, посёлок Октябрьский и город Октябрьский. Или одноимённые населённые пункты из разных районов - посёлок Фёдоровка рядом с Уфой и посёлок Фёдоровка в Фёдоровском районе. Но поскольку мне нужен поиск адресов только в 9 городах, то эти особенности я учитывать не стал.

Нормализация номера дома:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е".
Из неучтённых особенностей тут могут быть попытки вставить в поле номера дома слово "дом" или сокращение "д.", могут быть присутствовать слова "корпус", "корп.", "строение", "стр.", попытки вместо знака дроби написать слово "дробь" и т.п.

Нормализация названия улицы:
  1. Буквы переводятся в нижний регистр,
  2. Удаляются незначащие пробельные символы - символы в начале и конце строки, а подряд идущие пробельные символы заменяются на один пробел,
  3. Буква "ё" заменяется на "е",
  4. Получившаяся строка разбивается на последовательность слов, а границами слов считаются пробелы и точки. Это сделано для того, чтобы различные сокращения и инициалы отделились от слов, с которыми они написаны слитно,
  5. Удаляются одиночные буквы,

  6. Раскрываются сокращения "ул" -> "улица", "пер" -> "переулок", "пр" -> "проспект", "пер" -> "переулок", "бул" -> "бульвар", "пл" -> "площадь", "шос" -> "шоссе", "наб" -> "набережная", "им" -> "имени",

  7. От чисел отрезаются окончания, так что строки типа "60-летия", "2-й", "1-я" превращаются просто в числа,

  8. Удаляются незначащие слова типа "лет", "летия", "реки", "имени". Названия многих улиц приурочены к юбилеям каких-либо памятных событий ("50-летия Октября" или "60 лет СССР"). Набережные, естественно, часто имеют в своём названии названия рек, вдоль которых они расположены, поэтому между названием типа "набережная реки Уфы" или "набережная Уфы" нет никакой разницы. И, наконец, улицы часто называются в честь каких-то людей, поэтому нет разницы между названиями типа "проспект имени Ленина" или "проспект Ленина",

  9. Удаляются слова-классификаторы адреса типа "улица", "проспект", "площадь", "тракт", из которых запоминается только первое.

  10. Из оставшихся слов собирается нормализованный адрес, перед которым ставится слово-классификатор адреса.

  11. В получившейся строке ищутся идущие подряд пары слов типа "имя фамилия" или "титул фамилия", из которых остаётся только фамилия. Тут я делаю предположение, что в городе не бывает улиц одного класса, названных именами однофамильцев. То есть, в городе не может быть улицы Льва Толстого и улицы Алексея Толстого, но может быть улица Льва Толстого и проспект Алексея Толстого - в этом случае однофамильцы будут различаться классом улицы. И сюда же относятся различия в титулах - алгоритм нормализации не учитывает, что могут быть улицы академика Морозова и Павлика Морозова. Это преобразование помогает находить названия улиц, в случае если имя или титул человека, в честь которого названа искомая улица, не были указаны. Тут мне пришлось приложить усилия и составить список людей, именами которых названы улицы. У меня это единый список, но вообще, хорошо бы иметь отдельный список для каждого населённого пункта - так и точность и скорость нормализации будут выше. В России для этого можно использовать адресный справочник КЛАДР или пришедший ему на смену ФИАС - читайте, например КЛАДР умер, да здравствует ФИАС?

Также при поиске дома по адресу следует учитывать, что существуют угловые дома, которым часто назначаются сразу два адреса. В проекте OpenStreetMap нет единого соглашения по тому, каким образом в базе данных указывать такие адреса. Есть несколько разных подходов, которые описаны на этой странице: Key:addr. Угловые дома Для отображения информации на карте я использую JavaScript-библиотеку LeafLet, написанную киевским программистом Владимиром Агафонкиным. Эта библиотека отстаёт по возможностям от библиотеки OpenLayers, которая используется самим проектом OpenStreetMap, но мне она понравилась компактностью и простотой использования.

воскресенье, 12 августа 2012 г.

Установка renderd и mod_tile - системы отрисовки тайлов по запросу

Продолжение серии заметок, посвящённых настройке тайлового сервера. Смотрите также предыдущие заметки Подготовка карт для генератора тайлов Mapnik, Настройка базы данных для генератора тайлов Mapnik и Установка генератора тайлов Mapnik. На этот раз мы установим демон renderd и модуль mod_tile для веб-сервера Apache.

Скачиваем дистрибутив модуля mod_tile для apache2 и его бэкенд - демон renderd. mod_tile работает вместе с renderd. Когда браузер пользователя запрашивает тайл у веб-сервера apache2, запрос перенаправляется модулю mod_tile. Если mod_tile находит в кэше нужный тайл, и судя по метаинформации и настройкам кэширования, он не просрочен, mod_tile просто передаёт его apache2. Если же необходимый тайл отсутствует или просрочен, тогда mod_tile обращается по UNIX-сокету к renderd, чтобы тот отрисовал необходимый тайл, разместил его в файловой ситсеме и доложил о готовности тайла.

Скачиваем исходники модуля и демона:
$ svn co http://svn.openstreetmap.org/applications/utils/mod_tile/
Устанавливаем всё необходимое для его сборки:
# apt-get install debhelper apache2-mpm-prefork apache2-threaded-dev libmapnik2-dev autoconf automake m4 libtool
Собираем модуль и демон, и устанавливаем их:
$ cd mod_tile
$ dpkg-buildpackage -B -rfakeroot -us -uc
$ cd ..
# dpkg -i renderd_0.4-10~oneiric1_amd64.deb
# dpkg -i libapache2-mod-tile_0.4-10~oneiric1_amd64.deb
Редактируем файл конфигурации mod_tile /etc/apache2/sites-enabled/tileserver_site: пишем почтовый ящик администратора веб-сервера в опцию ServerAdmin, комментируем опции ServerName, ServerAlias.

Я также поменял порт с 80 на 8080 в опции VirtualHost, поскольку у меня Apache2 будет выступать в роли бэкэнда для более лёгкого сервера Lighttpd. Для этого я поменял порт Apache2 в файле /etc/ports.conf, в опциях NameVirtualHost и Listen.

Теперь нужно настроить renderd. Для этого откроем файл /etc/renderd.conf и поменяем значение опции plugins_dir в секции [mapnik] с /usr/lib/mapnik/0.7/input на /usr/lib/mapnik/2.0/input

Теперь можно перезапустить apache2 и renderd, чтобы их настройки вступили в силу:
# /etc/init.d/apache2 restart
# /etc/init.d/renderd restart
Я также подключил модуль mod_proxy в файле конфигурации Lighttpd /etc/lighttpd/lighttpd.conf:
server.modules += ( "mod_proxy" )
proxy.server = (
  "/osm/" =>
  (
    (
      "host" => "127.0.0.1",
      "port" => 8080
    )
  )
)
И перезапустил веб-сервер:
# /etc/init.d/lighttpd restart
Теперь, если всё сделано правильно, система должна заработать. Для того, чтобы убедиться в правильности настроек, поправим адрес сервера на тестовой странице /var/www/osm/slippymap.html - заменим с localhost на внешнее имя сервера servername.tld. Теперь в браузере переходим на страницу http://servername.tld/osm/slippymap.html и видим карту прямо из недр базы данных:

На этом настройка тайлового сервера закончена.

воскресенье, 5 августа 2012 г.

Установка генератора тайлов Mapnik

Продолжение серии заметок, посвящённых настройке тайлового сервера. Смотрите также предыдущие заметки Подготовка карт для генератора тайлов Mapnik и Настройка базы данных для генератора тайлов Mapnik.

На этот раз мы установим и настроим сам генератор тайлов, который по векторному описанию карты в базе данных будет генерировать растровые квадратные картинки, содержащие фрагменты карты в разных разрешениях.

Ставим Mapnik, готовим границы мира и береговые линии

Ставим генератор тайлов mapnik2:
# apt-get install python-mapnik2
Скачиваем береговые линии, границы мира и т.п. с помощью скрипта:
$ wget http://svn.openstreetmap.org/applications/rendering/mapnik/get-coastlines.sh
$ chmod +x get-coastlines.sh
$ ./get-coastlines.sh
Всё скачанное помещаем в каталог /usr/share/mapnik/world_boundaries:
# mkdir /usr/share/mapnik
# mv world_boundaries /usr/share/mapnik
Настраиваем стиль отрисовки карты

Скачиваем скрипты и файлы стилей Mapnik (может понадобиться установить subversion):
$ svn co http://svn.openstreetmap.org/applications/rendering/mapnik/
И удаляем лишнее:
$ find . -name .svn -exec rm -Rf \{\} \;
Перемещаем файлы стилей Mapnik в каталог /etc/mapnik-osm-data:
# mkdir /etc/mapnik-osm-data
# mv mapnik/inc mapnik/symbols mapnik/osm.xml /etc/mapnik-osm-data/
Переименовываем файлы шаблонов настроек:
# cd /etc/mapnik-osm-data/inc/
# mv fontset-settings.xml.inc.template fontset-settings.xml.inc
# mv datasource-settings.xml.inc.template datasource-settings.xml.inc
# mv settings.xml.inc.template settings.xml.inc
Исправляем настройки в соответствии с описанием Manually building a tile server.

В файле settings.xml.inc заменим:
1. <!ENTITY symbols "%(symbols)s">
на <ENTITY symbols "symbols">
2. <!ENTITY osm2pgsql_projection "&srs%(epsg)s;">
на <!ENTITY osm2pgsql_projection "&srs900913;">
3. <!ENTITY dwithin_node_way "&dwithin_%(epsg)s;">
на <!ENTITY dwithin_node_way "&dwithin_900913;">
4. <!ENTITY world_boundaries "%(world_boundaries)s">
на <!ENTITY world_boundaries "/usr/share/mapnik/world_boundaries">
5. <!ENTITY prefix "%(prefix)s">
на <!ENTITY prefix "planet_osm">

В файле datasource-settings.xml.inc прописываем настройки подключения к базе данных, заменив:
1. <Parameter name="password">%(password)</Parameter>
на <Parameter name="password">password</Parameter>
2. <Parameter name="host">%(host)s</Parameter>
закомментировав как
<!-- <Parameter name="host">%(host)s</Parameter> -->
3. <Parameter name="port">%(port)s</Parameter>
закомментировав как
<!-- <Parameter name="port">%(port)s</Parameter> -->
4. <Parameter name="user">%(user)s</Parameter>
на <Parameter name="user">osm</Parameter>
5. <Parameter name="dbname">%(dbname)s</Parameter>
на <Parameter name="dbname">osm</Parameter>
6. <Parameter name="estimate_extent">%(estimate_extent)s</Parameter>
на <Parameter name="estimate_extent">false</Parameter>
7. <Parameter name="extent">%(extent)s</Parameter>
на <Parameter name="extent">-20037508,-19929239,20037508,19929239</Parameter>

Здесь указываются настройки подключения к базе данных, а также указывается прямоугольник, в пределах которого умещаются все данные карты. По прямоугольнику Mapnik определяет, стоит ли вообще обращаться к базе данных за этими данными - быть может запрос заведомо ничего не вернёт и будет выполнен вхолостую.

Ну и источников данных тоже может быть несколько - несколько баз данных на разных серверах могут обслуживать данные какого-то определённого участка карты. Например, вполне возможно настроить два сервера, один из которых будет возвращать информацию по северному полушарию, а другой - по южному. В этом случае Mapnik по границам прямоугольника может определить, стоит ли обращаться к этому серверу.

Будьте крайне внимательны с настройками extent - если вы их укажете неправильно, Mapnik может не обращаться к базе данных, несмотря на то, что они в ней есть. Как следствие, Mapnik будет рисовать только береговые линии и границы мира.

Настройки адреса и порта сервера комментируем, поскольку сервер базы данных установлен на том же компьютере, что и Mapnik - подключение к серверу базы данных будет осуществляться через Unix-сокет.

Файл fontset-settings.xml.inc не меняем.

На этом настройка самого генератора тайлов Mapnik закончена. В следующей заметке я расскажу, как настроить верхушку всей инфраструктуры - тайловый сервер.

воскресенье, 29 июля 2012 г.

Настройка базы данных для генератора тайлов Mapnik

Продолжение серии заметок, посвящённых настройке тайлового сервера. Смотрите также предыдущую заметку Подготовка карт для генератора тайлов Mapnik.

Введение

Эта заметка посвящена настройке базы данных для генератора тайлов Mapnik. Генератор тайлов - это программа, которая на основе векторного описания карты и файла описания стилей формирует квадратные растровые картинки с изображением карты.

Генератор тайлов можно использовать в сочетании с различными JavaScript-библиотеками, позволяющими прямо в веб-браузере просматривать карту. Проект OpenStreetMap, в котором используется генератор тайлов Mapnik, пользуется JavaScript-бибилиотекой OpenLayers. Забегая вперёд хочу сказать, что мой выбор остановился на JavaScript-библиотеке Киевского программиста Владимира Агафонкина LeafLet, которая хотя и уступает библиотеке OpenLayers по возможностям, зато проще в использовании и имеет более качественную документацию.

Вообще, о начинке проекта OpenStreetMap могу сказать следующее. Видно что проект динамично развивается и не имеет какой-либо генеральной линии развития. Основные компоненты проекта написаны на самых разных языках: C++ и Python (Mapnik), Java (JOSM и Osmosis), JavaScript (OpenLayers, но тут уж без альтернатив), Ruby On Rails (веб-сайт OpenStreetMap). Одно время сайт OpenStreetMap хранил свои данные в MySQL, в то время как вся географическая информация хранилась в PostgreSQL. Однако, разработчики сайта всё-таки перешли полностью на PostgreSQL, отказавшись от MySQL.

Из-за такого буйства технологий и энергичности развития бывает довольно трудно подобрать согласованные версии программ, которые бы хорошо работали друг с другом, а документация часто оказывается либо не полной, либо просто устаревшей. Есть много побочных и альтернативных проектов, которые тоже частично устарели, а частично потеряли совместимость с современными версиями программ.

Для облегчения настройки я советую взять Debian 7.0 Wheezy, хотя и на Debian 6.0 Squeeze настройка тоже возможна, но она сопряжена с бОльшими усилиями (подключение сторонних репозиториев, пересборка пакетов и т.п.). Всё дальнейшее описание делается применительно именно к Debian 7.0 Wheezy.

Установка СУБД

Установим пакеты, необходимые нам на этом этапе:
# apt-get install postgresql postgresql-contrib postgresql-9.1-postgis osm2pgsql
Настраиваем авторизацию по паролю в файле /etc/postgresql/9.1/main/pg_hba.conf, заменив первую строчку на вторую:
local all all peer
local all all password
Теперь выполним тюнинг ядра Linux для увеличения скорости работы PostgreSQL:
# sysctl kernel.shmmax=268435456
И пропишем такую же настройку в файле /etc/sysctl.conf:
kernel.shmmax=268435456
После тюнинга ядра можно произвести тюнинг самого сервера PostgreSQL. Для этого в файле /etc/postgresql/9.1/main/postgresql.conf изменим следующие строчки:
shared_buffers = 128MB
checkpoint_segments = 20
maintenance_work_mem = 256MB
autovacuum = off
Перезапустим сервер PostgreSQL, чтобы новые настройки вступили в силу:
# /etc/init.d/postgresql restart
Создание базы данных для Mapnik.

Теперь создадим пользователя, базу данных, установим в неё расширения, необходимые для работы Mapnik. Войдём под администратором PostgreSQL:
# su - postgres
Создадим пользователя:
$ createuser -PRDS osm
Создадим базу данных, принадлежащую только что созданному пользователю:
$ createdb -E UTF8 -O osm osm
Установим в базу данных расширение для индексирования геометрических объектов, находящееся в пакете postgresql-contrib:
$ psql -d osm -c "CREATE EXTENSION btree_gist;"
Установим в базу данных расширение PostGIS, необходимое для осуществления различных операций над геометрическими объектами. Расширение находится в пакете postgresql-9.1-postgis. Подробнее почитать о функциях, добавляемых PostGIS в PostgreSQL, можно на официальном сайте проекта: PostGIS 1.5.4 Manual.
$ psql -d osm -f /usr/share/postgresql/9.1/contrib/postgis-1.5/postgis.sql
Делаем пользователя osm владельцем таблиц geometry_columns и spatial_ref_sys, чтобы osm2pgsql мог импортировать карты от имени пользователя osm:
$ psql -d osm -c "ALTER TABLE geometry_columns OWNER TO osm; ALTER TABLE spatial_ref_sys OWNER TO osm;"
Теперь нужно установить описания проекций. Самому Mapnik'у нужна лишь одна проекция - 900913, проекция Меркатора. Установить её можно с помощью следующей команды:
$ psql -d osm -f /usr/share/doc/osm2pgsql/examples/900913.sql
Я же установил все доступные проекции, так как в дальнейшем я с помощью SQL-запросов извлекал геометрическую информацию в проекции 4326 WGS84 (обычные широта и долгота), преобразовывая проекцию объектов с помощью функции PostGIS ST_Transform.
$ psql -d osm -f /usr/share/postgresql/9.1/contrib/postgis-1.5/spatial_ref_sys.sql
На этом настройка базы данных завершается, можно выйти из сеанса пользователя postgres, открытого командой su.

Заливка данных в базу данных Mapnik

Осталось залить карты в базу данных. Для этого можно воспользоваться утилитой из одноимённого пакета osm2pgsql:
$ osm2pgsql -U osm -d osm -W /home/stupin/Downloads/RU-BA_TA_ORE.pbf

воскресенье, 22 июля 2012 г.

Подготовка карт для генератора тайлов Mapnik

Это - первая заметка, посвящённая настройке и использованию генератора тайлов Mapnik. Генератор тайлов - это программа, создающая квадратные растровые изображения фрагментов карты, по её векторному описанию. Прежде чем настраивать сам генератор тайлов, нужно подготовить для него данные, чем мы и займёмся в этой заметке.

Заливка карт в базу данных Mapnik на первый взгляд не должна представлять каких-то сложностей - достаточно взять карту в формате с расширением osm, вооружиться утилитой osm2pgsql и вперёд. Но на самом деле даже в этом, на первый взгляд простом, деле, не имея никакого опыта, можно довольно долго ходить по плотно разложенным граблям.

Мне нужно было скачать и залить в базу данных Mapnik карты Башкирии, Татарстана и Оренбургской области. Я нашёл их на сайте gis-lab.ru, по ссылке http://gis-lab.info/projects/osm_dump/. Попробовал залить их в базу данных и был несколько разочарован - карты, выложенные там, устарели.

Дело в том, что сайт OpenStreetMap с 1 апреля 2012 года приостановил публикацию файлов карты планеты из-за смены лицензии: http://gis-lab.info/blog/2012-03/osm-license-change/. Была уже середина июня, а данные на сайте gis-lab.ru по-прежнему толком не обновлялись. И я решил вырезать из последнего дампа планеты интересующие меня области самостоятельно.

Для различных операций по обработке карт osm нам понадобится утилита osmosis. Лучше всего установить osmosis версии не ниже 0.40. Я поставил эту утилиту из репозитория Debian Wheezy (7.0), который на момент написания заметки был тестовой веткой Debian.

Скачиваем и готовим карту планеты

На скачивание архива размером 23 гигабайта со страницы http://planet.openstreetmap.org/ у меня ушло примерно 6 часов. Ещё 12 часов ушло на разжатие архива, а разжатый файл занял примерно 330 гигабайт.

В интернете я наткнулся на заметку *.osm or *.pbf ?, в которой сравнивались размеры файлов в этих форматах. Чтобы освободить место на диске и ускорить обработку данных, я решил преобразовать файл планеты в двоичный формат:
$ osmosis --read-xml file="planet-latest.osm" --write-pbf file="planet-latest.pbf" omitmetadata="yes"
Преобразование заняло примерно 25 часов, а двоичный файл занял всего-лишь 14 гигабайт.

Я очень советую проделать эту операцию, особенно если вы собираетесь работать с этим файлом в дальнейшем - накладывать файлы обновлений, вырезать из него различные области и т.п. Разница в нагрузке на диск по чтению уменьшается существенно и заметно даже на глаз. Ну и свободное место на диске лишним не бывает ;-)

Готовим полигон для обрезки карты

Теперь нам понадобятся файлы полигонов для обрезки интересующих нас областей. Я взял интересующие меня файлы с сайта gis-lab.ru, всё на той же странице http://gis-lab.info/projects/osm_dump/ по ссылке http://data.gis-lab.info/osm_dump/poly/.

Ещё нам понадобятся утилиты для преобразования файлов полигонов в файлы osm и обратно, которые можно взять здесь: http://svn.openstreetmap.org/applications/utils/osm-extract/polygons/ и редактор JOSM, который можно установить из дистрибутива.

Скачаем и подготовим к использованию скрипты:
$ wget http://svn.openstreetmap.org/applications/utils/osm-extract/polygons/poly2osm.pl
$ wget http://svn.openstreetmap.org/applications/utils/osm-extract/polygons/osm2poly.pl
$ chown +x poly2osm.pl osm2poly.pl
Установим редактор JOSM и его плагины:
# apt-get install josm josm-plugins
Программа постоянно обновляется, а свежую версию JOSM можно скачать с сайта josm.ru. Я рекомендую скачивать последнюю протестированную версию. Запустить её можно так:
$ java -jar josm-tested.jar
Преобразуем файлы полигонов в формат osm:
$ ./poly2osm.pl RU-BA.poly > RU-BA.osm
$ ./poly2osm.pl RU-TA.osm > RU-TA.osm
$ ./poly2osm.pl RU-ORE.osm > RU-ORE.osm
Теперь запустим редактор JOSM, и откроем в нём все три получившихся файла в отдельных слоях. Объединим слои, а затем воспользуемся инструментом "Объединить накладывающиеся полигоны". Выделим два любых полигона и нажмём Shift-J. Повторим операцию для всех оставшихся полигонов. В итоге получим один полигон, который сохраним в файл с расширением osm. Я сохранил под имененм RU-BA_TA_ORE.osm.


Осталось преобразовать файл из формата osm в формат полигона:
$ ./osm2poly.pl RU-BA_TA_ORE.osm > RU-BA_TA_ORE.poly
Вырезаем из карты планеты интересующую область

Теперь нужно обрезать карту планеты по полигону. Как выяснилось, в процессе обрезки карты планеты по полигону, osmosis создаёт временные файлы в каталоге /tmp. У меня этот каталог находится на корневом разделе, на котором было свободно всего 6 гигабайт. Программа работала довольно долго, а обнаружил я её уже завершившейся с сообщением о нехватке места без какой бы то ни было конкретики. При втором запуске я стал следить за местом на дисках, надеясь что программе не хватило именно его, а не места в оперативной памяти. Спустя несколько часов я увидел, что программа начала занимать место на корневом диске и нашёл её временные файлы в каталоге /tmp. Я поискал ответа в интернете и нашёл совет воспользоваться переменной окружения JAVACMD_OPTIONS. Как я узнал потом, временные файлы занимали примерно 24 гигабайта.

Кроме того, на карте могут существовать объекты, частично выходящие за границы полигона. Для того, чтобы эти объекты целиком попали в результирующую карту, нужно указать опции completeWays=true - дороги полностью и completeRelations=true - все объекты, относящиеся к вырезаемым (смысл этой опции мне не совсем понятен).

Для ускорения процесса вырезки карты рекомендуют воспользоваться опцией idTrackerType=BitSet, которая позволяет эффективно отслеживать идентификаторы объектов при вырезании из карты больших фрагментов. По умолчанию используется idTrackerType=idList, которая обладает какими-то ошибками в реализации, приводящими к высоким затратам времени на обрезку карты. Важно, что эту опцию необходимо указывать для каждой опции-фильтра (взята эта рекомендация отсюда: http://fprog.livejournal.com/5508.html?thread=166276). На моём компьютере osmosis при использовании этой опции завершался с ошибкой нехватки оперативной памяти, поэтому я не смог оценить прирост скорости обрезки карты.

Итоговая команда для обрезки карты у меня получилась такой:
$ env JAVACMD_OPTIONS="-Djava.io.tmpdir=/home/stupin/Downloads" \
osmosis --read-pbf file=planet-latest.pbf \
--bounding-polygon file=RU-BA_TA_ORE.poly completeWays=true \
--write-pbf file=RU-BA_TA_ORE.pbf omitmetadata=true

воскресенье, 15 июля 2012 г.

TwinView: монитор и телевизор

Почти ровно год назад я писал о настройке двухмониторной конфигурации X-сервера в заметке Xinerama: монитор и телевизор.

До недавних пор я был, в принципе, почти всем доволен. Меня лишь иногда беспокоило то, что при запуске из консоли некоторые программы ругались на отсутствие поддержки расширения XRandr. Это расширение конфликтует с конфигурацией Xinerama, и поэтому оно автоматически отключается. Многие программы, видимо, рассчитаны на работу именно с расширением XRandr, но не умеют работать с Xinerama. В чём заключалось это неумение - не понятно, всё работало нормально, за исключением этих последних китайских предупреждений.

В комментариях к моей предпоследней заметке "Настройка X-сервера" virens, автор блога Записки дебианщика, сказал, что ему будет интересно почитать о моём опыте настройки двухмониторной конфигурации с помощью XRandr. И сегодня я решил попробовать настроить XRandr.

К сожалению, настроить двухмониторную конфигурацию с помощью Xrandr мне не удалось - проприетарный драйвер nvidia, которым я пользуюсь, не поддерживает XRandr. (Тут я оставлю место для тухлых помидоров.) В своё оправдание могу сказать, что моему компьютеру уже почти 6 лет, а 6 лет назад покупка видеокарты ATI для компьютера с Linux неизбежно приводила к захватывающим приключениям на мягкое место. Хотя, говорят, что совсем недавно nvidia всё-таки сподобилась выпустить драйверы с поддержкой XRandr (Тестовая версия проприетарного драйвера NVIDIA 302.07 с поддержкой RandR 1.2/1.3), но эта новость скорее всего не работает для моего случая с видеокартой, которая поддерживается только legacy-драйвером nvidia. Но зато в проприетарном драйвере nvidia есть свой собственный велосипед для двухмониторых (и только) конфигураций под названием TwinView. Его-то я и попробовал настроить.

Это оказалось довольно простым делом, если вооружиться Переводом документации драйвера NVIDIA для Linux. Всё нужное у меня уже было готово после настройки Xinerama. Что я сделал:

1. Закоментировал опции
Screen     1   "Screen1" RightOf "Screen0"
и
Option         "Xinerama" "On"
в секции ServerLayout,

2. Закоментировал секции Device, Monitor и Screen, связанные с настройками второго монитора (то бишь телевизора),

3. Вписал в оставшуюся секцию Device следующие опции:
Option      "TwinView"
Option      "TwinViewOrientation" "RigthOf"
Option      "SecondMonitorHorizSync" "15.625 - 15.625"
Option      "SecondMonitorVertSync"  "50.0 - 50.0"
Option      "MetaModes" "1280x1024,800x600; 1024x768, 800x600; 800x600, 800x600; 640x480, 800x600"
4. Перезапустил дисплейный менеджер.

Как это ни странно, но всё заработало. Появились даже несколько плюсов. Поскольку у двух экранов разное разрешение по вертикали, то под вторым экраном образовывалась некая область, которую я условно назвал "мёртвая зона":
.________________.___________.
|                |           |
|                | Телевизор |
|    Монитор     |___________|
|                |  Мёртвая  |
|________________|___зона____|

Плюс первый - теперь мышь не уезжает из монитора в мёртвую зону, а останавливается прямо перед "мёртвой зоной", на краю экрана монитора. Если же мышь поднять повыше, то она легко проваливается на экран телевизора. Это удобно, потому что так мышь не теряется там, где ей нечего делать.

Плюс второй - раньше программы для снятия снимков экрана в "мёртвой зоне" помещали какую-то искажённую копию изображения на мониторе. Теперь в этой области на снимках виден просто чёрный прямоугольник (см. снимок ниже).

Ну и третий плюс - программы перестали ругаться на отсутствие поддержки XRandr.

Теперь снимки экрана выглядят вот так:

А мой третий файл конфигурации X-сервера для случая настройки двухмониторной конфигурации с использованием TwinView, выглядит вот так (закоментированные фрагменты вырезаны):
Section "ServerFlags"
  Option "AutoAddDevices" "False"
  Option "AllowEmptyInput" "False"
  Option "DontZap" "False"
EndSection

Section "ServerLayout"
  Identifier     "X.org Configured"
  Screen      0  "Screen0" 0 0
  InputDevice    "Mouse0" "CorePointer"
  InputDevice    "Keyboard0" "CoreKeyboard"
EndSection

Section "Files"
  #RgbPath      "/etc/X11/rgb"
  ModulePath   "/usr/lib/xorg/modules"
  FontPath     "/usr/share/fonts/X11/misc"
  FontPath     "/usr/share/fonts/X11/Type1"
  FontPath     "/var/lib/defoma/x-ttcidfont-conf.d/dirs/TrueType"
EndSection

Section "Module"
  Load  "dri"
  Load  "GLcore"
  Load  "glx"
  Load  "dbe"
  Load  "record"
  Load  "extmod"
  Load  "xtrap"
EndSection

Section "InputDevice"
  Identifier  "Keyboard0"
  Driver      "kbd"
  Option      "XkbRules" "xorg"
  Option      "XkbModel" "pc104"
  Option      "XkbLayout" "us,ru"
  Option      "XkbOptions" "grp:alt_shift_toggle"
  Option      "XkbVariant" ",winkeys"
  Option      "AutoRepeat" "250 30"
EndSection

Section "InputDevice"
  Identifier  "Mouse0"
  Driver      "mouse"
  Option      "Protocol" "auto"
  Option      "Device" "/dev/input/mice"
  Option      "ZAxisMapping" "4 5 6 7"
EndSection

Section "Monitor"
  DisplaySize  338 270 # mm
  Identifier   "Monitor0"
  VendorName   "LG"
  ModelName    "Flatron L1730S"
  ### Comment all HorizSync and VertRefresh values to use DDC:
  HorizSync    30.0 - 80.2
  VertRefresh  56.0 - 75.0
  Option       "DPMS"
  # 1280x1024 @ 75.00 Hz (GTF) hsync: 80.17 kHz; pclk: 138.54 MHz
  Modeline "1280x1024_75.00"  138.54  1280 1368 1504 1728  1024 1025 1028 1069  -HSync +Vsync
  # 1024x768 @ 75.00 Hz (GTF) hsync: 60.15 kHz; pclk: 81.80 MHz
  Modeline "1024x768_75.00"  81.80  1024 1080 1192 1360  768 769 772 802  -HSync +Vsync
  # 800x600 @ 75.00 Hz (GTF) hsync: 47.02 kHz; pclk: 48.91 MHz
  Modeline "800x600_75.00"  48.91  800 840 920 1040  600 601 604 627  -HSync +Vsync
  # 640x480 @ 75.00 Hz (GTF) hsync: 37.65 kHz; pclk: 30.72 MHz
  Modeline "640x480_75.00"  30.72  640 664 728 816  480 481 484 502  -HSync +Vsync
EndSection

Section "Device"
  Identifier  "Card0"
  Driver      "nvidia"
  VendorName  "nVidia Corporation"
  BoardName   "NV43 [GeForce 6600]"
  BusID       "PCI:5:0:0"
  Option      "TwinView"
  Option      "TwinViewOrientation" "RigthOf"
  Option      "SecondMonitorHorizSync" "15.625 - 15.625"
  Option      "SecondMonitorVertSync"  "50.0 - 50.0"
  Option      "MetaModes" "1280x1024,800x600; 1024x768, 800x600; 800x600, 800x600; 640x480, 800x600"
EndSection

Section "Screen"
  Identifier "Screen0"
  Device     "Card0"
  Monitor    "Monitor0"
  DefaultDepth    24
  SubSection "Display"
    Viewport   0 0
    Depth     24
    Modes "1280x1024_75.00" "1024x768_75.00" "800x600_75.00" "640x480_75.00"
  EndSubSection
EndSection