Создание простейшего HTTP-сервиса для публикации векторных данных: различия между версиями
(Новая страница: «{{Статья|Черновик}} == Введение == При разработки Веб-ГИС зачастую возникает необходимост…») |
Нет описания правки |
||
Строка 63: | Строка 63: | ||
выберем какой-нибудь Веб-фреймворк. Остановимся на [http://bottlepy.org/docs/dev/ Bottle]. Данный фреймворк уже использовался нами в статье | выберем какой-нибудь Веб-фреймворк. Остановимся на [http://bottlepy.org/docs/dev/ Bottle]. Данный фреймворк уже использовался нами в статье | ||
[http://gis-lab.info/qa/dynamic-tms.html Основы работы динамических TMS-сервисов]. | [http://gis-lab.info/qa/dynamic-tms.html Основы работы динамических TMS-сервисов]. | ||
== Установка программного обеспечения == | |||
=== Установка Bottle === | |||
Установим Bottle в [http://guide.python-distribute.org/virtualenv.html виртуальное окружение]: | |||
<pre> | |||
sudo aptitude install python-virtualenv | |||
cd ~ | |||
virtualenv --no-site-packages geoservice | |||
source geoservice/bin/activate | |||
pip install bottle | |||
pip install waitress | |||
</pre> | |||
=== Установка psycopg2 === | |||
<pre>pip install psycopg2</pre> | |||
В случае возникновения ошибок при установке требуется установка необходимых библиотек на уровне операционной системы, | |||
[http://stackoverflow.com/a/5450183/813758 подробнее]. | |||
=== Установка библиотеки для парсинга конфигурационного файла === | |||
<pre>pip install pyyaml</pre> | |||
== Создание сервиса == | |||
Переходим в директорию нашего виртуального окружения geoservice: | |||
<pre>cd geoservice</pre> | |||
и создаём в ней файл geoservice.py, в котором и будет располагаться код будущего HTTP-сервиса, также здесь поместим файл, содержащий описание подключения | |||
к нашей базе данных config.yaml: | |||
<pre> | |||
touch geoservice.py | |||
touch config.yaml | |||
</pre> | |||
Открываем файл config.yaml и помещаем в него следующее содержимое: | |||
<syntaxhighlight lang="yaml"> | |||
db: | |||
host: gis-lab.info | |||
port: 5432 | |||
name: gedetdom | |||
table: data | |||
user: guest | |||
password: guest | |||
geometry_column: geometry | |||
</syntaxhighlight> | |||
Затем открываем файл geoservice.py и пишем в него: | |||
<syntaxhighlight lang="python"> | |||
# -*- encoding: utf-8 -*- | |||
import json | |||
import psycopg2 | |||
from bottle import route, response, request, run | |||
from yaml import load | |||
# Остальной код будет располагаться тут | |||
if __name__ == "__main__": | |||
run(host='0.0.0.0', port=8087, server='waitress') | |||
</syntaxhighlight> | |||
=== Подключение к базе данных === | |||
Извлекаем информацию о подключении из конфигурационного файла: | |||
<syntaxhighlight lang="python"> | |||
db_config_file = open('config.yaml', 'rb') | |||
db_config = load(db_config_file).get('db') | |||
</syntaxhighlight> | |||
И подключаемся к базе данных: | |||
<syntaxhighlight lang="python"> | |||
conn_string = 'host=%(host)s dbname=%(name)s user=%(user)s password=%(password)s' | |||
conn_string %= db_config | |||
conn = psycopg2.connect(conn_string) | |||
cursor = conn.cursor() | |||
</syntaxhighlight> | |||
=== Извлечение данных из базы === | |||
Опишем функцию, которая будет извлекать значения GET-параметров из URL, | |||
осуществлять запрос к базе данных и передавать данные обратно клиенту: | |||
<syntaxhighlight lang="python"> | |||
@route('/features') | |||
def geoservice(bbox='-180,-90,180,90', epsg='4326'): | |||
# Параметры запроса | |||
bbox = (request.GET.get('bbox') or bbox).split(',') | |||
epsg = request.GET.get('epsg') or epsg | |||
attrs = request.GET.get('attrs',[]) | |||
coordinates = dict(xmin=bbox[0],ymin=bbox[1],xmax=bbox[2],ymax=bbox[3]) | |||
geometry_column = db_config.get('geometry_column') | |||
table = db_config.get('table') | |||
# Строка запроса | |||
query = """select st_asgeojson(st_transform({gc}, {epsg})) as g, * | |||
from {table} | |||
where st_transform({gc}, {epsg}) && st_makeenvelope({xmin},{ymin},{xmax},{ymax},{epsg}); | |||
""".format(gc=geometry_column, epsg=epsg, table=table, **coordinates) | |||
# Выполнение запроса | |||
cursor.execute(query) | |||
records = cursor.fetchall() | |||
# Формируем GeoJSON вручную | |||
collection = {'type': 'FeatureCollection', 'features': []} | |||
for rec in records: | |||
feature = dict() | |||
feature['type'] = 'Feature' | |||
feature['properties'] = dict() | |||
# Функция, возвращающая список вида [(0, attr1), (1, attr2), ..., (n, attrn)], | |||
# то есть осуществляется сопоставление имен и индексов полей | |||
columns = lambda a: zip(range(len(a)), a) | |||
# Итерация по полям | |||
for colnum, col in columns(cursor.description): | |||
if col.name == 'g': | |||
feature['geometry'] = json.loads(rec[colnum]) | |||
continue | |||
if col.name in attrs: | |||
feature['properties'][col.name] = rec[colnum] | |||
collection['features'].append(feature) | |||
response.content_type = 'application/json' | |||
return json.dumps(collection) | |||
</syntaxhighlight> | |||
=== Запуск сервиса === | |||
<pre> | |||
$ python geoservice.py | |||
Bottle v0.11.6 server starting up (using WaitressServer())... | |||
Listening on http://0.0.0.0:8087/ | |||
Hit Ctrl-C to quit. | |||
serving on http://0.0.0.0:8087 | |||
</pre> | |||
== Результаты == | |||
Пришло время проверить, что же получилось. Откроем браузер и протестируем следующие URL. | |||
<pre>http://localhost:8087/features?bbox=4180824.1850282,7488851.5571727,4187192.3449474,7490776.8148227&epsg=3857&attrs=post,name</pre> | |||
Результат: | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"type": "FeatureCollection", | |||
"features": [ | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
4184578.0278406707, | |||
7489606.364854654 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "117303, Москва, ул. Каховка, 2", | |||
"name": "Школа-интернат № 24 дя детей-сирот" | |||
} | |||
}, | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
4186044.5508123846, | |||
7488978.3756106505 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "117452, Москва, Симферопольский б-р, 20", | |||
"name": "Школа-интернат № 95 общеобразовательная" | |||
} | |||
} | |||
] | |||
} | |||
</syntaxhighlight> | |||
Как можно увидеть получены данные удовлетворяющие заданному охвату в системе координат EPSG:3857. | |||
<pre>http://debian:8087/features?bbox=82,53,83,54&attrs=post,name</pre> | |||
Результат: | |||
<syntaxhighlight lang="json"> | |||
{ | |||
"type": "FeatureCollection", | |||
"features": [ | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
82.995135, | |||
53.320683 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "659000, Алтайский край, с.Павловск, ул. Шумилова, 1", | |||
"name": "Павловский детский дом" | |||
} | |||
}, | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
82.984438, | |||
53.311395 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "659000, Алтайский край, с.Павловск, ул. Коминтерна, 2", | |||
"name": "Павловская школа-интернат спец.корр." | |||
} | |||
}, | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
82.470054, | |||
53.443182 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "659053, Алтайский край, Шелаболихинский р-н, с.Кучук, ул. Новая, 15", | |||
"name": "Кучукский детский дом" | |||
} | |||
}, | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
82.316406, | |||
53.784103 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "633623, Новосибирская обл., п. Сузун, ул. Толстого, 11", | |||
"name": "Социальный приют для детей и подростков \"Лесовичек\"" | |||
} | |||
}, | |||
{ | |||
"geometry": { | |||
"type": "Point", | |||
"coordinates": [ | |||
82.316406, | |||
53.784103 | |||
] | |||
}, | |||
"type": "Feature", | |||
"properties": { | |||
"post": "633610, Новосибирская обл., п. Сузун, ул. Партизанская, 19", | |||
"name": "Сузунская школа-интернат для детей-сирот 8ого вида" | |||
} | |||
} | |||
] | |||
} | |||
</syntaxhighlight> | |||
Для представления JSON-данных в удобочитаемом варианте можно воспользоватся сервисом [http://jsonlint.com/ JSONLint], что мы собственно и сделали. | |||
== Заключение == | |||
Таким образом, мы создали простейший HTTP-сервис, который на входе принимает определенный набор параметров и возвращает результат в формате GeoJSON. И хотя это | |||
всего-лишь учебный вариант, в котором не предусмотрена никакая обработка ошибок, но он наглядно показывает как может быть устроен простейший сервис подобного | |||
типа. Данные, предоставляемые этим сервисом, легко могут быть визуализированы с помощью картографического JavaScript-клиента, например, OpenLayers. |
Версия от 09:59, 13 июня 2013
Введение
При разработки Веб-ГИС зачастую возникает необходимость в передаче векторных данных с сервера на клиентскую сторону. При этом, как правило, векторные данные имеют большой объем или находятся в различных хранилищах и поэтому вариант передачи данных в виде одного файла сразу же исключается. Очевидно, что требуется некоторое промежуточное звено, которое бы могло получать от клиентского приложения запрос данных, удовлетворяющих определенному критерию (например, попадающие в некий охват), подключаться к хранилищу данных, извлекать нужный набор и возвращать его клиенту.
Для решения данной задачи существует специальный протокол OGC Web Feature Service, о котором мы рассказывали в одной из предыдущих статей. С одной стороны, опубликовав свои данные по WFS, мы получаем очень богатые возможности:
- отображение данных в настольных ГИС;
- гибкий язык описания фильтров;
- возможность редактирования данных.
Если бы перед нами стояла задача просто опубликовать некоторый набор данных по WFS, то никаких проблем - берем TinyOWS, MapServer или GeoServer, настраиваем подключение к хранилищу - всё. Но это не наш случай, нам нужно добавить WFS-функционал непосредственно в наше приложение. Как поступить в этом случае? Вариантов на самом деле не много: либо найти необходимую библиотеку на том языке программирования, на котором ведется разработка, либо каким-то образом "прикрутить" имеющийся WFS-сервер к нашему приложению. Не беремся утверждать за другие языки программирования, но, например, в случае Python не существует нативных библиотек, реализующих функционал WFS-сервиса, только лишь баиндинги к mapscript, а это по сути сводится к установке библиотек MapServer (который написан на C/C++). Завязывание же своего приложения на внешний по отношению к нему WFS-сервер - задача непростая и вызывающая ряд очень серьезных проблем:
- необходимость подготовки дистрибутива WFS-сервера под целевую платформу;
- язык программирования, используемый при разработке основного приложения и WFS-сервера зачастую не совпадают, следовательно в случае возникновение проблем с последним, необходимо привлечение дополнительных ресурсов;
- сама задача по интеграции приложение с WFS-сервером далеко не тривиальная.
Поэтому, если вы все-таки хотите использовать возможности WFS в своем приложении, то наиболее правильным видится подход в разработке соответствующего ПО на том же языке программирования, что используется в вашем приложении. В этом случае вы имеете полный контроль над своей системой.
Как показывает практика, для разработки небольших картографических Веб-приложений функционал, предлагаемый спецификацией WFS, крайне избыточен. Зачастую нам не нужно ни отображать наши данные в настольных ГИС, ни иметь гибкий язык описания фильтров, поэтому в большинстве случаев достаточно написания собственного простейшего HTTP-сервиса, возвращающего данные в каком-нибудь стандартном формате (например,GeoJSON). Именно решению данной задачи и будет посвящена оставшаяся часть статьи. В качестве примера использования подобного сервиса может служить проект Найди участкового, откройте консоль вашего браузера и посмотрите какие запросы уходят на сервер при сдвиге карты.
В качестве языка программирования будем использовать Python, операционная система - Debian GNU/Linux 7.0.
Выбор API
Если вам требуется разработать полнофункциональный HTTP-сервис, то чтобы не изобретать велосипед, можно взять готовое описание какого-нибудь API (в зависимости от задач) и реализовать его, например, MapFish Protocol или ArcGIS Server REST API (открытая реализация HTTP-сервиса, реализующая данный API уже существует).
В нашем случае мы разработаем HTTP-сервис, поддерживающий 3 GET-параметра:
- bbox={xmin,ymin,xmax,ymax} - запрашиваемый bbox (по умолчанию считается, что координаты указаны в системе координат EPSG:4326);
- epsg={num} - система координат в которой должны быть возвращены данные, также используется как система координат для bbox;
- attrs={field1}[,{field2},...] - имена запрашиваемых атрибутивных полей.
Хранилище и формат выходных данных
В качестве данных возьмем БД PostGIS, созданную в рамках проекта по созданию слоя детских учреждений. Выходной формат - GeoJSON.
При разработке Веб-ГИС стандартная практика заключается в передаче данных клиенту в формате GeoJSON, при этом объект GeoJSON - это объект типа "FeatureCollection" - коллекция элементарных объектов. Объект типа "FeatureCollection" содержит одно свойство "features", значение данного свойства – массив, каждый элемент которого представляет собой элементарный объект.
В PostgreSQL, начиная с версии 9.2, появились специальные функции для работы с JSON-данными, в частности row_to_json, позволяющие, используя SQL-запрос к базе данных PostGIS, получить "FeatureCollection". О том как это сделать подробно описано в статье Creating GeoJSON Feature Collections with JSON and PostGIS functions, мы же ввиду того что используемое нами хранилище развернуто на PostgreSQL 9.1 будем формировать "FeatureCollection" самостоятельно в коде нашего сервиса.
Веб-фреймворк
Для того, чтобы облегчить себе жизнь и не писать служебный код в качестве инструмента для работы с HTTP-запросами выберем какой-нибудь Веб-фреймворк. Остановимся на Bottle. Данный фреймворк уже использовался нами в статье Основы работы динамических TMS-сервисов.
Установка программного обеспечения
Установка Bottle
Установим Bottle в виртуальное окружение:
sudo aptitude install python-virtualenv cd ~ virtualenv --no-site-packages geoservice source geoservice/bin/activate pip install bottle pip install waitress
Установка psycopg2
pip install psycopg2
В случае возникновения ошибок при установке требуется установка необходимых библиотек на уровне операционной системы, подробнее.
Установка библиотеки для парсинга конфигурационного файла
pip install pyyaml
Создание сервиса
Переходим в директорию нашего виртуального окружения geoservice:
cd geoservice
и создаём в ней файл geoservice.py, в котором и будет располагаться код будущего HTTP-сервиса, также здесь поместим файл, содержащий описание подключения к нашей базе данных config.yaml:
touch geoservice.py touch config.yaml
Открываем файл config.yaml и помещаем в него следующее содержимое:
db:
host: gis-lab.info
port: 5432
name: gedetdom
table: data
user: guest
password: guest
geometry_column: geometry
Затем открываем файл geoservice.py и пишем в него:
# -*- encoding: utf-8 -*-
import json
import psycopg2
from bottle import route, response, request, run
from yaml import load
# Остальной код будет располагаться тут
if __name__ == "__main__":
run(host='0.0.0.0', port=8087, server='waitress')
Подключение к базе данных
Извлекаем информацию о подключении из конфигурационного файла:
db_config_file = open('config.yaml', 'rb')
db_config = load(db_config_file).get('db')
И подключаемся к базе данных:
conn_string = 'host=%(host)s dbname=%(name)s user=%(user)s password=%(password)s'
conn_string %= db_config
conn = psycopg2.connect(conn_string)
cursor = conn.cursor()
Извлечение данных из базы
Опишем функцию, которая будет извлекать значения GET-параметров из URL, осуществлять запрос к базе данных и передавать данные обратно клиенту:
@route('/features')
def geoservice(bbox='-180,-90,180,90', epsg='4326'):
# Параметры запроса
bbox = (request.GET.get('bbox') or bbox).split(',')
epsg = request.GET.get('epsg') or epsg
attrs = request.GET.get('attrs',[])
coordinates = dict(xmin=bbox[0],ymin=bbox[1],xmax=bbox[2],ymax=bbox[3])
geometry_column = db_config.get('geometry_column')
table = db_config.get('table')
# Строка запроса
query = """select st_asgeojson(st_transform({gc}, {epsg})) as g, *
from {table}
where st_transform({gc}, {epsg}) && st_makeenvelope({xmin},{ymin},{xmax},{ymax},{epsg});
""".format(gc=geometry_column, epsg=epsg, table=table, **coordinates)
# Выполнение запроса
cursor.execute(query)
records = cursor.fetchall()
# Формируем GeoJSON вручную
collection = {'type': 'FeatureCollection', 'features': []}
for rec in records:
feature = dict()
feature['type'] = 'Feature'
feature['properties'] = dict()
# Функция, возвращающая список вида [(0, attr1), (1, attr2), ..., (n, attrn)],
# то есть осуществляется сопоставление имен и индексов полей
columns = lambda a: zip(range(len(a)), a)
# Итерация по полям
for colnum, col in columns(cursor.description):
if col.name == 'g':
feature['geometry'] = json.loads(rec[colnum])
continue
if col.name in attrs:
feature['properties'][col.name] = rec[colnum]
collection['features'].append(feature)
response.content_type = 'application/json'
return json.dumps(collection)
Запуск сервиса
$ python geoservice.py Bottle v0.11.6 server starting up (using WaitressServer())... Listening on http://0.0.0.0:8087/ Hit Ctrl-C to quit. serving on http://0.0.0.0:8087
Результаты
Пришло время проверить, что же получилось. Откроем браузер и протестируем следующие URL.
http://localhost:8087/features?bbox=4180824.1850282,7488851.5571727,4187192.3449474,7490776.8148227&epsg=3857&attrs=post,name
Результат:
{
"type": "FeatureCollection",
"features": [
{
"geometry": {
"type": "Point",
"coordinates": [
4184578.0278406707,
7489606.364854654
]
},
"type": "Feature",
"properties": {
"post": "117303, Москва, ул. Каховка, 2",
"name": "Школа-интернат № 24 дя детей-сирот"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
4186044.5508123846,
7488978.3756106505
]
},
"type": "Feature",
"properties": {
"post": "117452, Москва, Симферопольский б-р, 20",
"name": "Школа-интернат № 95 общеобразовательная"
}
}
]
}
Как можно увидеть получены данные удовлетворяющие заданному охвату в системе координат EPSG:3857.
http://debian:8087/features?bbox=82,53,83,54&attrs=post,name
Результат:
{
"type": "FeatureCollection",
"features": [
{
"geometry": {
"type": "Point",
"coordinates": [
82.995135,
53.320683
]
},
"type": "Feature",
"properties": {
"post": "659000, Алтайский край, с.Павловск, ул. Шумилова, 1",
"name": "Павловский детский дом"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
82.984438,
53.311395
]
},
"type": "Feature",
"properties": {
"post": "659000, Алтайский край, с.Павловск, ул. Коминтерна, 2",
"name": "Павловская школа-интернат спец.корр."
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
82.470054,
53.443182
]
},
"type": "Feature",
"properties": {
"post": "659053, Алтайский край, Шелаболихинский р-н, с.Кучук, ул. Новая, 15",
"name": "Кучукский детский дом"
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
82.316406,
53.784103
]
},
"type": "Feature",
"properties": {
"post": "633623, Новосибирская обл., п. Сузун, ул. Толстого, 11",
"name": "Социальный приют для детей и подростков \"Лесовичек\""
}
},
{
"geometry": {
"type": "Point",
"coordinates": [
82.316406,
53.784103
]
},
"type": "Feature",
"properties": {
"post": "633610, Новосибирская обл., п. Сузун, ул. Партизанская, 19",
"name": "Сузунская школа-интернат для детей-сирот 8ого вида"
}
}
]
}
Для представления JSON-данных в удобочитаемом варианте можно воспользоватся сервисом JSONLint, что мы собственно и сделали.
Заключение
Таким образом, мы создали простейший HTTP-сервис, который на входе принимает определенный набор параметров и возвращает результат в формате GeoJSON. И хотя это всего-лишь учебный вариант, в котором не предусмотрена никакая обработка ошибок, но он наглядно показывает как может быть устроен простейший сервис подобного типа. Данные, предоставляемые этим сервисом, легко могут быть визуализированы с помощью картографического JavaScript-клиента, например, OpenLayers.