Создание простейшего HTTP-сервиса для публикации векторных данных: различия между версиями

Материал из GIS-Lab
Перейти к навигации Перейти к поиску
мНет описания правки
Нет описания правки
 
(не показано 28 промежуточных версий 3 участников)
Строка 1: Строка 1:
{{Статья|Черновик}}
{{Статья|Опубликована|http-publish-vector}}
{{Аннотация|В статье представлен простейший вариант реализации HTTP-сервиса, позволяющий клиенту запрашивать данные на определенный охват в указанной системе координат, некий упрощенный вариант запроса GetFeature, определенного в рамках OGC Web Feature Service.}}


== Введение ==
== Введение ==


При разработки Веб-ГИС зачастую возникает необходимость в передаче векторных данных с сервера на клиентскую сторону. При этом, как правило, векторные
При разработке Веб-ГИС зачастую возникает необходимость в передаче векторных данных с сервера на клиентскую сторону. При этом, как правило, векторные
данные имеют большой объем или находятся в различных хранилищах и поэтому требуется некоторое промежуточное звено, которое бы могло получать от клиентского приложения запрос данных, удовлетворяющих определенному критерию (например, попадающие в некий охват), подключаться к хранилищу данных, извлекать нужный набор и возвращать его клиенту.
данные имеют большой объем или находятся в различных хранилищах и поэтому требуется некоторое промежуточное звено, которое бы могло получать от клиентского приложения запрос данных, удовлетворяющих определенному критерию (например, попадающие в некий охват), подключаться к хранилищу данных, извлекать нужный набор и возвращать его клиенту.


Для решения данной задачи существует специальный протокол OGC Web Feature Service, о котором мы рассказывали в одной из предыдущих [http://gis-lab.info/qa/wfs-begin.html статей]. С одной стороны, опубликовав свои данные по WFS, мы получаем очень богатые возможности:
Для решения данной задачи существует специальный протокол [http://www.opengeospatial.org/standards/wfs OGC Web Feature Service], о котором мы рассказывали в одной из предыдущих [http://gis-lab.info/qa/wfs-begin.html статей]. С одной стороны, опубликовав свои данные по WFS, мы получаем очень богатые возможности:


* отображение данных в настольных ГИС;
* отображение данных в настольных ГИС;
Строка 16: Строка 17:
WFS-функционал непосредственно в наше приложение. Как поступить в этом случае? Вариантов на самом деле не много: либо найти необходимую библиотеку на том языке
WFS-функционал непосредственно в наше приложение. Как поступить в этом случае? Вариантов на самом деле не много: либо найти необходимую библиотеку на том языке
программирования, на котором ведется разработка, либо каким-то образом "прикрутить" имеющийся WFS-сервер к нашему приложению. Не беремся утверждать за другие языки
программирования, на котором ведется разработка, либо каким-то образом "прикрутить" имеющийся WFS-сервер к нашему приложению. Не беремся утверждать за другие языки
программирования, но, например, в случае Python не существует нативных библиотек, реализующих функционал WFS-сервиса, только лишь баиндинги к
программирования, но, например, в случае Python не существует нативных библиотек, реализующих функционал WFS-сервиса, только лишь обертки к [http://mapserver.org/mapscript/introduction.html mapscript], а это по сути сводится к установке библиотек MapServer (который написан на C/C++). Завязывание же своего приложения на внешний по отношению к нему WFS-сервер - задача непростая и тянущая за собой ряд очень серьезных проблем:
[http://mapserver.org/mapscript/introduction.html mapscript], а это по сути сводится к установке библиотек MapServer (который написан на C/C++). Завязывание
же своего приложения на внешний по отношению к нему WFS-сервер - задача непростая и вызывающая ряд очень серьезных проблем:


* необходимость подготовки дистрибутива WFS-сервера под целевую платформу;
* необходимость подготовки дистрибутива WFS-сервера под целевую платформу;
Строка 27: Строка 26:
же языке программирования, что используется в вашем приложении. В этом случае вы имеете полный контроль над своей системой.
же языке программирования, что используется в вашем приложении. В этом случае вы имеете полный контроль над своей системой.


Как показывает практика, для разработки небольших картографических Веб-приложений функционал, предлагаемый спецификацией WFS, крайне избыточен. Зачастую нам не нужно
Как показывает практика, для разработки небольших картографических Веб-приложений функционал, предлагаемый спецификацией WFS, крайне избыточен. Зачастую оказывается ненужным ни отображение данных в настольных ГИС, ни гибкий язык описания фильтров, поэтому в большинстве случаев достаточно написания собственного простейшего HTTP-сервиса, возвращающего данные в каком-нибудь стандартном формате (например,[http://gis-lab.info/docs/geojson_ru.html GeoJSON]). Именно решению данной
ни отображать наши данные в настольных ГИС, ни иметь гибкий язык описания фильтров, поэтому в большинстве случаев достаточно написания собственного простейшего HTTP-сервиса, возвращающего данные в каком-нибудь стандартном формате (например,[http://gis-lab.info/docs/geojson_ru.html GeoJSON]). Именно решению данной
задачи и будет посвящена оставшаяся часть статьи. В качестве примера использования подобного сервиса может служить проект [http://demo.nextgis.ru/openpolice/ Найди участкового], откройте консоль вашего браузера и посмотрите, какие запросы уходят на сервер и какие ответы приходят от него при сдвиге карты. По виду URL мы можем однозначно сказать, что клиент и сервер взаимодействуют друг с другом, не используя протокол WFS, вместо этого используется некий самописный HTTP-сервис со своим API.
задачи и будет посвящена оставшаяся часть статьи. В качестве примера использования подобного сервиса может служить проект [http://demo.nextgis.ru/openpolice/ Найди участкового], откройте консоль вашего браузера и посмотрите какие запросы уходят на сервер при сдвиге карты.


В качестве языка программирования будем использовать Python, операционная система - Debian GNU/Linux 7.0.
В качестве языка программирования будем использовать Python, операционная система - Debian GNU/Linux 7.0.
Строка 35: Строка 33:
== Выбор API ==
== Выбор API ==


Если вам требуется разработать полнофункциональный HTTP-сервис, то чтобы не изобретать велосипед, можно взять готовое описание какого-нибудь
Если вам требуется разработать полнофункциональный HTTP-сервис, то, чтобы не изобретать велосипед, можно взять готовое описание какого-нибудь
API (в зависимости от задач) и реализовать его, например,
API (в зависимости от задач) и реализовать его. Например,
[http://trac.mapfish.org/trac/mapfish/wiki/MapFishProtocol MapFish Protocol] или [http://resources.arcgis.com/en/help/rest/apiref/index.html?featureserver.html ArcGIS Server REST API] (открытая реализация HTTP-сервиса, реализующая данный API уже [http://gis-lab.info/qa/mapfeatureserver.html существует]).
[http://trac.mapfish.org/trac/mapfish/wiki/MapFishProtocol MapFish Protocol] (реализован в [https://papyrus.readthedocs.org/en/latest/ Papyrus]) или [http://resources.arcgis.com/en/help/rest/apiref/index.html?featureserver.html ArcGIS Server REST API] (открытая реализация HTTP-сервиса, реализующая данный API уже [http://gis-lab.info/qa/mapfeatureserver.html существует]).


В нашем случае мы разработаем HTTP-сервис, поддерживающий 3 GET-параметра:
В нашем случае мы разработаем HTTP-сервис, поддерживающий 3 GET-параметра:
Строка 55: Строка 53:


В PostgreSQL, начиная с версии 9.2, появились специальные функции для работы с JSON-данными, в частности [http://www.postgresql.org/docs/devel/static/functions-json.html row_to_json], позволяющие, используя SQL-запрос к базе данных PostGIS, получить "FeatureCollection". О том как это сделать подробно описано в статье
В PostgreSQL, начиная с версии 9.2, появились специальные функции для работы с JSON-данными, в частности [http://www.postgresql.org/docs/devel/static/functions-json.html row_to_json], позволяющие, используя SQL-запрос к базе данных PostGIS, получить "FeatureCollection". О том как это сделать подробно описано в статье
[http://www.postgresonline.com/journal/archives/267-Creating-GeoJSON-Feature-Collections-with-JSON-and-PostGIS-functions.html Creating GeoJSON Feature Collections with JSON and PostGIS functions], мы же ввиду того что используемое нами хранилище развернуто на PostgreSQL 9.1 будем формировать "FeatureCollection" самостоятельно в коде
[http://www.postgresonline.com/journal/archives/267-Creating-GeoJSON-Feature-Collections-with-JSON-and-PostGIS-functions.html Creating GeoJSON Feature Collections with JSON and PostGIS functions], мы же, ввиду того что используемое нами хранилище развернуто на PostgreSQL 9.1, будем формировать "FeatureCollection" самостоятельно в коде
нашего сервиса.
нашего сервиса.


== Веб-фреймворк ==
== Веб-фреймворк ==


Для того, чтобы облегчить себе жизнь и не писать служебный код в качестве инструмента для работы с HTTP-запросами
Для того, чтобы облегчить себе жизнь и не писать служебный код, в качестве инструмента для работы с HTTP-запросами
выберем какой-нибудь Веб-фреймворк. Остановимся на [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-сервисов].
Строка 85: Строка 83:
В случае возникновения ошибок при установке требуется установка необходимых библиотек на уровне операционной системы,
В случае возникновения ошибок при установке требуется установка необходимых библиотек на уровне операционной системы,
[http://stackoverflow.com/a/5450183/813758 подробнее].
[http://stackoverflow.com/a/5450183/813758 подробнее].
=== Установка bottle-pgsql ===
<pre>pip install bottle-pgsql</pre>
[https://github.com/raisoblast/bottle-pgsql bottle-pgsql] - это плагин, автоматизирующий работу с PostgreSQL в Bottle-приложение. При каждом запросе он автоматически
подключается к базе данных, передаёт в функцию обработки запроса ее дескриптор и в самом конце закрывает соединение. Результаты выполнения запросов представляются в виде словарей, а не в виде кортежей, как это происходит по умолчанию в psycopg2.


=== Установка библиотеки для парсинга конфигурационного файла ===
=== Установка библиотеки для парсинга конфигурационного файла ===
Строка 110: Строка 115:
   host: gis-lab.info
   host: gis-lab.info
   port: 5432
   port: 5432
   name: gedetdom
   name: geodetdom
   table: data
   table: data
   user: guest
   user: guest
Строка 122: Строка 127:
# -*- encoding: utf-8 -*-
# -*- encoding: utf-8 -*-
import json
import json
import psycopg2
import bottle_pgsql
from bottle import route, response, request, run
from bottle import install, route, response, request, run, static_file
from yaml import load
from yaml import load


Строка 139: Строка 144:
db_config_file = open('config.yaml', 'rb')
db_config_file = open('config.yaml', 'rb')
db_config = load(db_config_file).get('db')
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
</syntaxhighlight>
</syntaxhighlight>


И подключаемся к базе данных:
И подключаем плагин bottle-pgsql:


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
conn_string = 'host=%(host)s dbname=%(name)s user=%(user)s password=%(password)s'
plugin = bottle_pgsql.Plugin(conn_string)
conn_string %= db_config
install(plugin)
conn = psycopg2.connect(conn_string)
cursor = conn.cursor()
</syntaxhighlight>
</syntaxhighlight>


=== Извлечение данных из базы ===
=== Извлечение данных из базы ===


Опишем функцию, которая будет извлекать значения GET-параметров из URL,
Опишем функцию, которая будет извлекать значения GET-параметров из URL, осуществлять запрос к базе данных и передавать данные обратно клиенту:
осуществлять запрос к базе данных и передавать данные обратно клиенту:


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
@route('/features')
@route('/features')
def geoservice(bbox='-180,-90,180,90', epsg='4326'):
def geoservice(db, bbox='-180,-90,180,90', epsg='4326', limit='100'):
 
        # Параметры запроса
    # Параметры запроса
        bbox = (request.GET.get('bbox') or bbox).split(',')
    bbox = (request.GET.get('bbox') or bbox).split(',')
        epsg = request.GET.get('epsg') or epsg
    epsg = request.GET.get('epsg') or epsg
        attrs = request.GET.get('attrs', [])
    attrs = request.GET.get('attrs',[])
        coordinates = dict(xmin=bbox[0], ymin=bbox[1], xmax=bbox[2], ymax=bbox[3])
    coordinates = dict(xmin=bbox[0],ymin=bbox[1],xmax=bbox[2],ymax=bbox[3])
        geometry_column = db_config.get('geometry_column')
    geometry_column = db_config.get('geometry_column')
        table = db_config.get('table')
    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)
        query = """select st_asgeojson(st_transform({gc}, {epsg})) as g, *
    records = cursor.fetchall()
              from {table}
              where st_intersects(st_transform({gc}, {epsg}), st_makeenvelope({xmin},{ymin},{xmax},{ymax},{epsg}))
              limit {limit};
              """.format(gc=geometry_column, epsg=epsg, table=table, limit=limit, **coordinates)


    # Формируем GeoJSON вручную
        # Выполнение запроса
    collection = {'type': 'FeatureCollection', 'features': []}
         db.execute(query)
    for rec in records:
         records = db.fetchall()
         feature = dict()
         feature['type'] = 'Feature'
        feature['properties'] = dict()


         # Функция, возвращающая список вида [(0, attr1), (1, attr2), ..., (n, attrn)],
         # Формируем GeoJSON вручную
         # то есть осуществляется сопоставление имен и индексов полей
        collection = {'type': 'FeatureCollection', 'features': []}
        columns = lambda a: zip(range(len(a)), a)
         for rec in records:
            feature = dict()
            feature['type'] = 'Feature'
            feature['properties'] = dict()


        # Итерация по полям
            # Итерация по именам полей
        for colnum, col in columns(cursor.description):
            for colname in rec.keys():
            if col.name == 'g':
                if colname == 'g':
                feature['geometry'] = json.loads(rec[colnum])
                    feature['geometry'] = json.loads(rec[colname])
                continue
                    continue
            if col.name in attrs:
                if colname in attrs:
                feature['properties'][col.name] = rec[colnum]
                    feature['properties'][colname] = rec[colname]
        collection['features'].append(feature)
            collection['features'].append(feature)


    response.content_type = 'application/json'
        response.content_type = 'application/json'
    return json.dumps(collection)
        return json.dumps(collection)
</syntaxhighlight>
</syntaxhighlight>


Строка 256: Строка 256:
</syntaxhighlight>
</syntaxhighlight>


Как можно увидеть получены данные удовлетворяющие заданному охвату в системе координат EPSG:3857.
Как можно увидеть получены данные, удовлетворяющие заданному охвату в системе координат EPSG:3857.


<pre>http://debian:8087/features?bbox=82,53,83,54&attrs=post,name</pre>
<pre>http://localhost:8087/features?bbox=82,53,83,54&attrs=post,name</pre>


Результат:
Результат:
Строка 341: Строка 341:


Для представления JSON-данных в удобочитаемом варианте можно воспользоватся сервисом [http://jsonlint.com/ JSONLint], что мы собственно и сделали.
Для представления JSON-данных в удобочитаемом варианте можно воспользоватся сервисом [http://jsonlint.com/ JSONLint], что мы собственно и сделали.
== Визуализация данных в OpenLayers ==
По умолчанию используемый нами HTTP-сервер не умеет раздавать [http://bottlepy.org/docs/dev/tutorial.html#static-files статику]. Чтобы это исправить, откроем файл geoservice.py и добавим следующую функцию (в тот же уровень вложенности, на котором располагается функция geoservice):
<syntaxhighlight lang="python">
@route('/<filename:re:.*\.html>')
def htmls(filename):
    return static_file(filename, root='/home/rykovd/projects/geoservice/static')
</syntaxhighlight>
Данная функция будет возвращать любой документ с расширением *.html, находящийся в каталоге root.
В каталоге нашего виртуального окружения geoservice создадим директорию static и поместим в нее файл client.html, в котором будет располагаться код нашего Веб-клиента:
<pre>
mkdir static
cd static
touch client.html
</pre>
Открываем файл client.html и вставляем в него следующий код:
<syntaxhighlight lang="javascript">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Geodetdom</title>
<script src="http://openlayers.org/dev/OpenLayers.js"></script>
<script type="text/javascript">
function init(){
    var options = {
        div: "map",
        layers: [new OpenLayers.Layer.OSM()]
    };
    var  map = new OpenLayers.Map(options);
    geodetdom = new OpenLayers.Layer.Vector("Детские дома", {
        projection: new OpenLayers.Projection('EPSG:3857'),
        strategies: [new OpenLayers.Strategy.BBOX({ratio: 1, resFactor: 1})],
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8087/features",
            params: {
                attrs: 'id,name',
                epsg: '3857'
            },
            format: new OpenLayers.Format.GeoJSON()
        })
    });
    map.addLayer(geodetdom);
    map.zoomToExtent([3062373.1007906, 8962088.6911328, 18560133.457509, 9930698.7134278]);
}
</script>
</head>
<body onload="init()">
    <div id="map" style="width:100%; height:100%;"></div>
</body>
</html>
</syntaxhighlight>
В данном примере использована стратегия [http://dev.openlayers.org/releases/OpenLayers-2.12/doc/devdocs/files/OpenLayers/Strategy/BBOX-js.html OpenLayers.Strategy.BBOX], что означает, что данные будут запрашиваться при каждом изменении охвата карты. Опция resFactor, выставленная в значение 1, позволяет нам запрашивать данные даже в том случае, если новый охват попадает в предыдущий (например, при изменении масштаба карты), но с учетом того, что изменилось разрешение базового слоя. Стратегия BBOX автоматически создает фильтр слоя OpenLayers.Filter.Spatial.BBOX, что приводит к тому, что при запросе данных к URL автоматически добавляется GET-параметр bbox с указанием значения текущего охвата, что совпадает с именем GET-параметра, ответственного за охват, используемого в нашем сервисе.
[[Файл:Geoservice.png|581px|thumb|center|<center>Результат визуализации данных HTTP-сервиса в OpenLayers</center>]]
Если открыть консоль Веб-браузера и ввести следующие команды:
<pre>
geodetdom.features.length
100
geodetdom.features[1].attributes
Object {id: 8547, name: "Школа-интернат № 1 "}
</pre>
то можно увидеть, что количество объектов слоя не превосходит лимита, заданного на уровне сервиса (100 объектов), а также что, объекты содержат только запрошенные атрибуты.


== Заключение ==
== Заключение ==


Таким образом, мы создали простейший HTTP-сервис, который на входе принимает определенный набор параметров и возвращает результат в формате GeoJSON. И хотя это
Таким образом, мы создали простейший HTTP-сервис, который на входе принимает определенный набор параметров и возвращает результат в формате GeoJSON. И хотя это всего-лишь учебный вариант, в котором не предусмотрена никакая обработка ошибок, но он наглядно показывает как может быть устроен простейший сервис подобного типа. Данные, предоставляемые этим сервисом, легко могут быть визуализированы с помощью картографического JavaScript-клиента, например, OpenLayers.
всего-лишь учебный вариант, в котором не предусмотрена никакая обработка ошибок, но он наглядно показывает как может быть устроен простейший сервис подобного
типа. Данные, предоставляемые этим сервисом, легко могут быть визуализированы с помощью картографического JavaScript-клиента, например, OpenLayers.

Текущая версия от 13:25, 18 июня 2013

Эта страница опубликована в основном списке статей сайта
по адресу http://gis-lab.info/qa/http-publish-vector.html


В статье представлен простейший вариант реализации HTTP-сервиса, позволяющий клиенту запрашивать данные на определенный охват в указанной системе координат, некий упрощенный вариант запроса GetFeature, определенного в рамках OGC Web Feature Service.

Введение

При разработке Веб-ГИС зачастую возникает необходимость в передаче векторных данных с сервера на клиентскую сторону. При этом, как правило, векторные данные имеют большой объем или находятся в различных хранилищах и поэтому требуется некоторое промежуточное звено, которое бы могло получать от клиентского приложения запрос данных, удовлетворяющих определенному критерию (например, попадающие в некий охват), подключаться к хранилищу данных, извлекать нужный набор и возвращать его клиенту.

Для решения данной задачи существует специальный протокол 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). Именно решению данной задачи и будет посвящена оставшаяся часть статьи. В качестве примера использования подобного сервиса может служить проект Найди участкового, откройте консоль вашего браузера и посмотрите, какие запросы уходят на сервер и какие ответы приходят от него при сдвиге карты. По виду URL мы можем однозначно сказать, что клиент и сервер взаимодействуют друг с другом, не используя протокол WFS, вместо этого используется некий самописный HTTP-сервис со своим API.

В качестве языка программирования будем использовать Python, операционная система - Debian GNU/Linux 7.0.

Выбор API

Если вам требуется разработать полнофункциональный HTTP-сервис, то, чтобы не изобретать велосипед, можно взять готовое описание какого-нибудь API (в зависимости от задач) и реализовать его. Например, MapFish Protocol (реализован в Papyrus) или 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

В случае возникновения ошибок при установке требуется установка необходимых библиотек на уровне операционной системы, подробнее.

Установка bottle-pgsql

pip install bottle-pgsql

bottle-pgsql - это плагин, автоматизирующий работу с PostgreSQL в Bottle-приложение. При каждом запросе он автоматически подключается к базе данных, передаёт в функцию обработки запроса ее дескриптор и в самом конце закрывает соединение. Результаты выполнения запросов представляются в виде словарей, а не в виде кортежей, как это происходит по умолчанию в 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: geodetdom
  table: data
  user: guest
  password: guest
  geometry_column: geometry

Затем открываем файл geoservice.py и пишем в него:

# -*- encoding: utf-8 -*-
import json
import bottle_pgsql
from bottle import install, route, response, request, run, static_file
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

И подключаем плагин bottle-pgsql:

plugin = bottle_pgsql.Plugin(conn_string)
install(plugin)

Извлечение данных из базы

Опишем функцию, которая будет извлекать значения GET-параметров из URL, осуществлять запрос к базе данных и передавать данные обратно клиенту:

@route('/features')
def geoservice(db, bbox='-180,-90,180,90', epsg='4326', limit='100'):
        # Параметры запроса
        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_intersects(st_transform({gc}, {epsg}), st_makeenvelope({xmin},{ymin},{xmax},{ymax},{epsg}))
              limit {limit};
              """.format(gc=geometry_column, epsg=epsg, table=table, limit=limit, **coordinates)

        # Выполнение запроса
        db.execute(query)
        records = db.fetchall()

        # Формируем GeoJSON вручную
        collection = {'type': 'FeatureCollection', 'features': []}
        for rec in records:
            feature = dict()
            feature['type'] = 'Feature'
            feature['properties'] = dict()

            # Итерация по именам полей
            for colname in rec.keys():
                if colname == 'g':
                    feature['geometry'] = json.loads(rec[colname])
                    continue
                if colname in attrs:
                    feature['properties'][colname] = rec[colname]
            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://localhost: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, что мы собственно и сделали.

Визуализация данных в OpenLayers

По умолчанию используемый нами HTTP-сервер не умеет раздавать статику. Чтобы это исправить, откроем файл geoservice.py и добавим следующую функцию (в тот же уровень вложенности, на котором располагается функция geoservice):

@route('/<filename:re:.*\.html>')
def htmls(filename):
    return static_file(filename, root='/home/rykovd/projects/geoservice/static')

Данная функция будет возвращать любой документ с расширением *.html, находящийся в каталоге root.

В каталоге нашего виртуального окружения geoservice создадим директорию static и поместим в нее файл client.html, в котором будет располагаться код нашего Веб-клиента:

mkdir static
cd static
touch client.html

Открываем файл client.html и вставляем в него следующий код:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Geodetdom</title>
<script src="http://openlayers.org/dev/OpenLayers.js"></script>

<script type="text/javascript">
function init(){

    var options = {
        div: "map",
        layers: [new OpenLayers.Layer.OSM()]
    };
    var  map = new OpenLayers.Map(options);

    geodetdom = new OpenLayers.Layer.Vector("Детские дома", {
        projection: new OpenLayers.Projection('EPSG:3857'),
        strategies: [new OpenLayers.Strategy.BBOX({ratio: 1, resFactor: 1})],
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://localhost:8087/features",
            params: {
                attrs: 'id,name',
                epsg: '3857'
            },
            format: new OpenLayers.Format.GeoJSON()
        })
    });
    map.addLayer(geodetdom);
    map.zoomToExtent([3062373.1007906, 8962088.6911328, 18560133.457509, 9930698.7134278]);
}
</script>
</head>
<body onload="init()">
    <div id="map" style="width:100%; height:100%;"></div>
</body>
</html>

В данном примере использована стратегия OpenLayers.Strategy.BBOX, что означает, что данные будут запрашиваться при каждом изменении охвата карты. Опция resFactor, выставленная в значение 1, позволяет нам запрашивать данные даже в том случае, если новый охват попадает в предыдущий (например, при изменении масштаба карты), но с учетом того, что изменилось разрешение базового слоя. Стратегия BBOX автоматически создает фильтр слоя OpenLayers.Filter.Spatial.BBOX, что приводит к тому, что при запросе данных к URL автоматически добавляется GET-параметр bbox с указанием значения текущего охвата, что совпадает с именем GET-параметра, ответственного за охват, используемого в нашем сервисе.

Результат визуализации данных HTTP-сервиса в OpenLayers

Если открыть консоль Веб-браузера и ввести следующие команды:

geodetdom.features.length
100
geodetdom.features[1].attributes
Object {id: 8547, name: "Школа-интернат № 1 "}

то можно увидеть, что количество объектов слоя не превосходит лимита, заданного на уровне сервиса (100 объектов), а также что, объекты содержат только запрошенные атрибуты.

Заключение

Таким образом, мы создали простейший HTTP-сервис, который на входе принимает определенный набор параметров и возвращает результат в формате GeoJSON. И хотя это всего-лишь учебный вариант, в котором не предусмотрена никакая обработка ошибок, но он наглядно показывает как может быть устроен простейший сервис подобного типа. Данные, предоставляемые этим сервисом, легко могут быть визуализированы с помощью картографического JavaScript-клиента, например, OpenLayers.