Встраивание кэширующего TMS-сервиса в собственное приложение: различия между версиями
(Новая страница: «{{Статья|Черновик}} {{Аннотация|Рассмотрена процедура встраивания TileCache в собственное при…») |
Нет описания правки |
||
Строка 120: | Строка 120: | ||
то не должно появляться никаких сообщений об ошибках, что свидетельствует о том, | то не должно появляться никаких сообщений об ошибках, что свидетельствует о том, | ||
что системный Mapnik виден из виртуального окружения. | что системный Mapnik виден из виртуального окружения. | ||
== Хранение кэшированных данных в PostgreSQL == | |||
Среди [http://tilecache.org/docs/Caches.html списка поддерживаемых кэшей] | |||
в Tilecache отсутствуют те или иные СУБД. Конечно, в качестве учебного | |||
примера мы могли бы остановиться на использовании какого-нибудь стандартного | |||
кэша, например, файлового, но мы немного усложним задачу, реализовав собственный | |||
класс, отвечающий за хранение кэшированных данных в СУБД PostgreSQL. Будем | |||
считать, что PostgreSQL установлен на той же машине, что и разрабатываемое | |||
приложение. | |||
'''Создание базы данных''' | |||
Создадим на уровне БД отдельного пользователя, назовём его ''cacheuser'' | |||
(пароль ''secret''): | |||
<syntaxhighlight lang="bash"> | |||
sudo su postgres -c "createuser -P -e cacheuser" | |||
Введите пароль для новой роли: | |||
Повторите его: | |||
Должна ли новая роль иметь полномочия суперпользователя? (y - да/n - нет) n | |||
Новая роль должна иметь право создавать базы данных? (y - да/n - нет) n | |||
Новая роль должна иметь право создавать другие роли? (y - да/n - нет) n | |||
CREATE ROLE cacheuser PASSWORD 'md57ea40027c2db9dc1b1b85ad7bf0f5314' NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN; | |||
</syntaxhighlight> | |||
Мы создали пользователя, который не обладает правами суперпользователя, | |||
не может создавать базы данных и другие роли. От имени пользователя ''postgres'' создадим | |||
базу данных ''tilecache'' и сделаем пользователя ''cacheuser'' её владельцем: | |||
<syntaxhighlight lang="bash"> | |||
sudo su postgres -c "createdb -O cacheuser --encoding=UTF8 tilecache" | |||
</syntaxhighlight> | |||
'''Подключение базы данных в приложение''' | |||
Подключаем созданную БД в наше приложение, для этого в файле | |||
~/cache/cache/development.ini редактируем строку ''sqlalchemy.url'': | |||
<syntaxhighlight lang="bash"> | |||
sqlalchemy.url = postgresql+psycopg2://cacheuser:secret@localhost/tilecache | |||
</syntaxhighlight> | |||
'''Описание модели кэша''' | |||
Для взаимодействия нашего приложения с базой данных будем использовать ORM | |||
[http://www.sqlalchemy.org/ SQLAlchemy]. Для описания структуры | |||
таблицы базы данных, в которой будут храниться кэшированные данные, воспользуемся | |||
[http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative.html декларативным] | |||
синтаксисом SQLAlchemy. Для этого открываем файл ~/cache/cache/cache/models.py | |||
и помещаем в него следующее содержимое: | |||
<syntaxhighlight lang="python"> | |||
# -*- coding: utf-8 -*- | |||
from sqlalchemy import Column | |||
from sqlalchemy import Integer, Unicode, LargeBinary | |||
from sqlalchemy.orm import scoped_session | |||
from sqlalchemy.orm import sessionmaker | |||
from sqlalchemy.ext.declarative import declarative_base | |||
from zope.sqlalchemy import ZopeTransactionExtension | |||
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) | |||
Base = declarative_base() | |||
# Хранилище тайлов TMS-слоя | |||
class TileCache(Base): | |||
__tablename__ = 'tilecache' | |||
layer = Column(Unicode(50), primary_key=True) | |||
z = Column(Integer, primary_key=True) | |||
x = Column(Integer, primary_key=True) | |||
y = Column(Integer, primary_key=True) | |||
data = Column(LargeBinary) | |||
</syntaxhighlight> | |||
Из представленного кода видно, что кэшированные данные будут храниться в таблице | |||
''tilecache'', содержащей 5 полей: ''layer'' - поле, содержащее имя слой, тайл | |||
которого представлен в записи, ''x'', ''y'', ''z'' - координаты тайла, ''data'' - | |||
собственно сам тайл. Если в вашей базе данных установлен PostGIS, то имеет | |||
смысл добавить в эту таблицу дополнительное поле геометрии, которое будет | |||
содержать в себе значение охвата тайла. Это поле в дальнейшем очень удобно использовать, | |||
например, для очистки фрагмента кэша по какому-либо пространственному условию. | |||
'''Инициализация базы данных''' | |||
По описанному классу создадим таблицу в базе данных. Для этого откройте файл | |||
~/cache/cache/cache/scripts/initializedb.py и приведите его в соответствие со | |||
следующим фрагментом: | |||
<syntaxhighlight lang="python"> | |||
import os | |||
import sys | |||
import transaction | |||
from sqlalchemy import engine_from_config | |||
from pyramid.paster import ( | |||
get_appsettings, | |||
setup_logging, | |||
) | |||
from ..models import ( | |||
DBSession, | |||
Base | |||
) | |||
def usage(argv): | |||
cmd = os.path.basename(argv[0]) | |||
print('usage: %s <config_uri>\n' | |||
'(example: "%s development.ini")' % (cmd, cmd)) | |||
sys.exit(1) | |||
def main(argv=sys.argv): | |||
if len(argv) != 2: | |||
usage(argv) | |||
config_uri = argv[1] | |||
setup_logging(config_uri) | |||
settings = get_appsettings(config_uri) | |||
engine = engine_from_config(settings, 'sqlalchemy.') | |||
DBSession.configure(bind=engine) | |||
Base.metadata.create_all(engine) | |||
</syntaxhighlight> | |||
После этого выполните команду: | |||
<syntaxhighlight lang="bash"> | |||
initialize_cache_db ~/cache/cache/development.ini | |||
</syntaxhighlight> | |||
Должно появиться следующее сообщение, свидетельствующее о том, что таблица | |||
успешно создана: | |||
<syntaxhighlight lang="bash"> | |||
2013-07-17 12:05:08,945 INFO [sqlalchemy.engine.base.Engine][MainThread] select version() | |||
2013-07-17 12:05:08,945 INFO [sqlalchemy.engine.base.Engine][MainThread] {} | |||
2013-07-17 12:05:08,961 INFO [sqlalchemy.engine.base.Engine][MainThread] select current_schema() | |||
2013-07-17 12:05:08,962 INFO [sqlalchemy.engine.base.Engine][MainThread] {} | |||
2013-07-17 12:05:08,980 INFO [sqlalchemy.engine.base.Engine][MainThread] select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname=current_schema() and relname=%(name)s | |||
2013-07-17 12:05:08,980 INFO [sqlalchemy.engine.base.Engine][MainThread] {'name': u'tilecache'} | |||
2013-07-17 12:05:08,993 INFO [sqlalchemy.engine.base.Engine][MainThread] | |||
CREATE TABLE tilecache ( | |||
layer VARCHAR(50) NOT NULL, | |||
z SERIAL NOT NULL, | |||
x INTEGER NOT NULL, | |||
y INTEGER NOT NULL, | |||
data BYTEA, | |||
PRIMARY KEY (layer, z, x, y) | |||
) | |||
2013-07-17 12:05:08,998 INFO [sqlalchemy.engine.base.Engine][MainThread] {} | |||
2013-07-17 12:05:09,286 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT | |||
</syntaxhighlight> |
Версия от 09:32, 17 июля 2013
Рассмотрена процедура встраивания TileCache в собственное приложение, а также приведет пример создания кэша на базе PostgreSQL.
Введение
Предположим, что вы разрабатываете клиент-серверную Веб-ГИС и вам требуется отображать на клиенте некоторую растровую подложку, которая создается на базе данных хранилища (БД), к которому имеет доступ серверная компонента. Стандартным решением подобной задачи является опубликование данных по протоколу TMS и подключение их на клиенте. TMS-сервис может функционировать в одном из двух режимов: статическом и динамическом. И если вы планируете в дальнейшем тиражировать свое приложение, то использование статического TMS будет заключаться либо в предварительном создании набора тайлов (в случае, если данные, на основе которых будет создаваться TMS-слой, одинаковы для всех инсталляций приложения, например, данные о государственных границах) и распространении их вместе с приложением (что может привести к недопустимому росту размера приложения), либо в предоставлении пользователю инструмента по генерированию тайлов "на месте". Последний вариант в принципе не плох, но его недостаток заключается в том, что пользователю для того чтобы начать работать с вашим приложением потребуется время на заполнение кэша (может занимать от нескольких часов до нескольких дней). Выходом из данного положения служит использование динамического TMS-сервиса, генерирующего тайлы по запросу. И тут возникает проблема. Существующее ПО для создания динамических TMS-сервисов (TileCache, MapProxy) является самостоятельным программным обеспечением и изначально не предназначено для использования в качестве встраиваемых решений. Использовать же MapProxy или TileCache по их прямому назначению в данном случае - не вариант, так как это сводится к тому, что пользователь должен помимо установки вашего приложения установить и настроить тайловый сервер, что может оказаться ему не под силу, да и это очень неудобно. Поэтому решение данной задачи сводится к написанию собственного TMS-сервиса и интеграции его в приложение. Пример решения подобной задачи был рассмотрен в статье Основы работы динамических TMS-сервисов, но основным недостатком получившегося там сервиса является то, что он не поддерживает процедуру кэширования, что очень важно при разработке реального приложения.
В рамках данной статьи рассмотрим создание кэширующего TMS-сервиса на базе классов, предоставляемых TileCache. В качестве рендерера будем использовать - Mapnik. TileCache был выбран в качестве базовой системы в виду того, что он имеет довольно простую и понятную архитектуру в отличие от того же MapProxy, хотя по функционалу в целом значительно уступает последнему.
В качестве языка программирования будем использовать Python, операционная система - Debian GNU/Linux 7.0.
Создание каркаса приложения
Во избежание написания большого количества служебного кода в качестве каркаса нашего приложения будем использовать Веб-фреймворк Pyramid.
Создание каталога будущего проекта
mkdir ~/cache
cd ~/cache
Создание виртуального окружения
virtualenv --no-site-packages env
Установка Pyramid
source env/bin/activate
pip install pyramid
Генерирование структуры проекта
pcreate -s alchemy cache
В файл ~/cache/cache/setup.py в массив requires к имеющимся пакетам добавляем имена пакетов, которые будут использоваться в нашем проекте: psycopg2 и TileCache.
Установка проекта в режиме разработки
cd cache
python setup.py develop
Установка Mapnik
Идейно верное решение - это указать Mapnik как зависимость в файле setup.py для его автоматической установки, но на практике установка Mapnik в виртуальное окружение представляет собой довольно сложную задачу, поэтому для перехода к следующему шагу установите Mapnik в систему, после чего сделайте симлинк на директорию с Python-пакетами виртуального окружения. Для того, чтобы узнать, куда был установлен Mapnik, запустите системный Python и выполните следующие команды:
import mapnik
mapnik.__file__
'/usr/lib/python2.7/dist-packages/mapnik/__init__.py'
Как можно видеть, в нашем случае Mapnik был установлен в каталог /usr/lib/python2.7/dist-packages/mapnik. Находясь в активном виртуальном окружении, создаем симлинк:
ln -s /usr/lib/python2.7/dist-packages/mapnik $VIRTUAL_ENV/lib/python2.7/site-packages/mapnik
Теперь, если запустить Python в виртуальном окружении и дать команду:
import mapnik
то не должно появляться никаких сообщений об ошибках, что свидетельствует о том, что системный Mapnik виден из виртуального окружения.
Хранение кэшированных данных в PostgreSQL
Среди списка поддерживаемых кэшей в Tilecache отсутствуют те или иные СУБД. Конечно, в качестве учебного примера мы могли бы остановиться на использовании какого-нибудь стандартного кэша, например, файлового, но мы немного усложним задачу, реализовав собственный класс, отвечающий за хранение кэшированных данных в СУБД PostgreSQL. Будем считать, что PostgreSQL установлен на той же машине, что и разрабатываемое приложение.
Создание базы данных
Создадим на уровне БД отдельного пользователя, назовём его cacheuser (пароль secret):
sudo su postgres -c "createuser -P -e cacheuser"
Введите пароль для новой роли:
Повторите его:
Должна ли новая роль иметь полномочия суперпользователя? (y - да/n - нет) n
Новая роль должна иметь право создавать базы данных? (y - да/n - нет) n
Новая роль должна иметь право создавать другие роли? (y - да/n - нет) n
CREATE ROLE cacheuser PASSWORD 'md57ea40027c2db9dc1b1b85ad7bf0f5314' NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;
Мы создали пользователя, который не обладает правами суперпользователя, не может создавать базы данных и другие роли. От имени пользователя postgres создадим базу данных tilecache и сделаем пользователя cacheuser её владельцем:
sudo su postgres -c "createdb -O cacheuser --encoding=UTF8 tilecache"
Подключение базы данных в приложение
Подключаем созданную БД в наше приложение, для этого в файле ~/cache/cache/development.ini редактируем строку sqlalchemy.url:
sqlalchemy.url = postgresql+psycopg2://cacheuser:secret@localhost/tilecache
Описание модели кэша
Для взаимодействия нашего приложения с базой данных будем использовать ORM SQLAlchemy. Для описания структуры таблицы базы данных, в которой будут храниться кэшированные данные, воспользуемся декларативным синтаксисом SQLAlchemy. Для этого открываем файл ~/cache/cache/cache/models.py и помещаем в него следующее содержимое:
# -*- coding: utf-8 -*-
from sqlalchemy import Column
from sqlalchemy import Integer, Unicode, LargeBinary
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from zope.sqlalchemy import ZopeTransactionExtension
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
# Хранилище тайлов TMS-слоя
class TileCache(Base):
__tablename__ = 'tilecache'
layer = Column(Unicode(50), primary_key=True)
z = Column(Integer, primary_key=True)
x = Column(Integer, primary_key=True)
y = Column(Integer, primary_key=True)
data = Column(LargeBinary)
Из представленного кода видно, что кэшированные данные будут храниться в таблице tilecache, содержащей 5 полей: layer - поле, содержащее имя слой, тайл которого представлен в записи, x, y, z - координаты тайла, data - собственно сам тайл. Если в вашей базе данных установлен PostGIS, то имеет смысл добавить в эту таблицу дополнительное поле геометрии, которое будет содержать в себе значение охвата тайла. Это поле в дальнейшем очень удобно использовать, например, для очистки фрагмента кэша по какому-либо пространственному условию.
Инициализация базы данных
По описанному классу создадим таблицу в базе данных. Для этого откройте файл ~/cache/cache/cache/scripts/initializedb.py и приведите его в соответствие со следующим фрагментом:
import os
import sys
import transaction
from sqlalchemy import engine_from_config
from pyramid.paster import (
get_appsettings,
setup_logging,
)
from ..models import (
DBSession,
Base
)
def usage(argv):
cmd = os.path.basename(argv[0])
print('usage: %s <config_uri>\n'
'(example: "%s development.ini")' % (cmd, cmd))
sys.exit(1)
def main(argv=sys.argv):
if len(argv) != 2:
usage(argv)
config_uri = argv[1]
setup_logging(config_uri)
settings = get_appsettings(config_uri)
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
Base.metadata.create_all(engine)
После этого выполните команду:
initialize_cache_db ~/cache/cache/development.ini
Должно появиться следующее сообщение, свидетельствующее о том, что таблица успешно создана:
2013-07-17 12:05:08,945 INFO [sqlalchemy.engine.base.Engine][MainThread] select version()
2013-07-17 12:05:08,945 INFO [sqlalchemy.engine.base.Engine][MainThread] {}
2013-07-17 12:05:08,961 INFO [sqlalchemy.engine.base.Engine][MainThread] select current_schema()
2013-07-17 12:05:08,962 INFO [sqlalchemy.engine.base.Engine][MainThread] {}
2013-07-17 12:05:08,980 INFO [sqlalchemy.engine.base.Engine][MainThread] select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname=current_schema() and relname=%(name)s
2013-07-17 12:05:08,980 INFO [sqlalchemy.engine.base.Engine][MainThread] {'name': u'tilecache'}
2013-07-17 12:05:08,993 INFO [sqlalchemy.engine.base.Engine][MainThread]
CREATE TABLE tilecache (
layer VARCHAR(50) NOT NULL,
z SERIAL NOT NULL,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
data BYTEA,
PRIMARY KEY (layer, z, x, y)
)
2013-07-17 12:05:08,998 INFO [sqlalchemy.engine.base.Engine][MainThread] {}
2013-07-17 12:05:09,286 INFO [sqlalchemy.engine.base.Engine][MainThread] COMMIT